Llamar a AngularJS desde el código heredado

180

Estoy usando AngularJS para crear controles HTML que interactúan con una aplicación Flex heredada. Todas las devoluciones de llamada de la aplicación Flex se deben adjuntar a la ventana DOM.

Por ejemplo (en AS3)

ExternalInterface.call("save", data);

Llamará

window.save = function(data){
    // want to update a service 
    // or dispatch an event here...
}

Desde la función de cambio de tamaño de JS, me gustaría enviar un evento que un controlador pueda escuchar. Parece que crear un servicio es el camino a seguir. ¿Se puede actualizar un servicio desde fuera de AngularJS? ¿Puede un controlador escuchar eventos de un servicio? En un experimento (haga clic para violín) que hice, parece que puedo acceder a un servicio, pero la actualización de los datos del servicio no se refleja en la vista (en el ejemplo, se <option>debe agregar a <select>).

¡Gracias!

kreek
fuente
1
Tenga en cuenta que en el jsfiddle sobre el inyector se obtiene sin apuntar a un elemento dentro de la aplicación usando var injector = angular.injector(['ng', 'MyApp']);. Hacer esto le dará un contexto completamente nuevo y un duplicado myService. Eso significa que terminará con dos instancias del servicio y el modelo y agregará datos al lugar equivocado. En su lugar, debe apuntar a un elemento dentro de la aplicación utilizando angular.element('#ng-app').injector(['ng', 'MyApp']). En este punto, puede usar $ apply para ajustar los cambios del modelo.
Thanh Nguyen

Respuestas:

293

La interoperabilidad desde fuera de angular a angular es igual a la depuración de la aplicación angular o la integración con la biblioteca de terceros.

Para cualquier elemento DOM puede hacer esto:

  • angular.element(domElement).scope() para obtener el alcance actual del elemento
  • angular.element(domElement).injector() para obtener el inyector de la aplicación actual
  • angular.element(domElement).controller()para obtener la ng-controllerinstancia.

Desde el inyector puede obtener cualquier servicio en aplicaciones angulares. Del mismo modo, desde el alcance puede invocar cualquier método que se le haya publicado.

Tenga en cuenta que cualquier cambio en el modelo angular o cualquier invocación de método en el alcance deben incluirse de $apply()esta manera:

$scope.$apply(function(){
  // perform any model changes or method invocations here on angular app.
});
Misko Hevery
fuente
1
esto funciona, pero desearía que hubiera alguna forma de pasar de un Módulo directamente a su alcance, ¿es eso posible? Tener que retroceder y seleccionar el [ng-app]nodo raíz parece estar al revés cuando ya tengo una referencia al Módulo ...
mindplay.dk
55
No puedo hacer que esto funcione: estoy llamando angular.element(document.getElementById(divName)).scope(), pero no puedo invocar ninguna función, simplemente devuelve "indefinido" en la consola.
Emil
1
Incluso si estoy enfrentando el mismo problema descrito anteriormente por @Emil, está regresando indefinido. Alguna ayuda ?
Bibin
55
element (). scope () no funcionará si los datos de depuración están desactivados, que es la recomendación para la producción. ¿No lo hace inútil en este escenario? Esto será solo para pruebas / depuración.
K. Norbert
44
No querrás hacer esto en Angular 1.3. El equipo de Angular no tenía la intención de llamarnos ".scope ()" en elementos del código de producción. Estaba destinado a ser una herramienta de depuración. Entonces, comenzando en Angular 1.3, puede desactivar esto. Angular dejará de adjuntar el alcance al elemento utilizando la función .data de jQuery. Esto acelerará su aplicación. Además, entregar sus ámbitos a las características de almacenamiento en caché de jquery creará pérdidas de memoria. Por lo tanto, definitivamente debe desactivar esto para acelerar su aplicación. El sitio de Angular tiene una guía de producción que debe usar para obtener más información.
helado
86

Misko dio la respuesta correcta (obviamente), pero algunos de nosotros, los novatos, podemos necesitar que se simplifique aún más.

Cuando se trata de llamar al código AngularJS desde aplicaciones heredadas, piense en el código AngularJS como una "micro aplicación" existente dentro de un contenedor protegido en su aplicación heredada. No puede realizar llamadas directamente (por una muy buena razón), pero puede realizar llamadas remotas a través del objeto $ scope.

Para usar el objeto $ scope, necesita obtener el identificador de $ scope. Afortunadamente esto es muy fácil de hacer.

Puede usar la identificación de cualquier elemento HTML dentro de su HTML de "micro-aplicación" AngularJS para obtener el identificador de la aplicación AngularJS $ scope.

Como ejemplo, digamos que queremos llamar a un par de funciones dentro de nuestro controlador AngularJS como sayHi () y sayBye (). En el AngularJS HTML (vista) tenemos un div con el id "MySuperAwesomeApp". Puede usar el siguiente código, combinado con jQuery para obtener el identificador de $ scope:

var microappscope = angular.element($("#MySuperAwesomeApp")).scope();

Ahora puede llamar a sus funciones de código AngularJS a través del identificador de alcance:

// we are in legacy code land here...

microappscope.sayHi();

microappscope.sayBye();

Para hacer las cosas más convenientes, puede usar una función para tomar el control del alcance en cualquier momento que desee acceder a él:

function microappscope(){

    return angular.element($("#MySuperAwesomeApp")).scope();

}

Sus llamadas se verían así:

microappscope().sayHi();

microappscope().sayBye();

Puedes ver un ejemplo de trabajo aquí:

http://jsfiddle.net/peterdrinnan/2nPnB/16/

También mostré esto en una presentación de diapositivas para el grupo Ottawa AngularJS (solo salte a las últimas 2 diapositivas)

http://www.slideshare.net/peterdrinnan/angular-for-legacyapps

Peter Drinnan
fuente
44
Tenga en cuenta que se desaconsejan las respuestas de solo enlace, las respuestas SO deberían ser el punto final de una búsqueda de una solución (frente a otra escala de referencias, que tienden a quedarse obsoletas con el tiempo). Considere agregar una sinopsis independiente aquí, manteniendo el enlace como referencia.
kleopatra
Buena aclaración adicional. Gracias.
Joe
1
Hermosa explicación! me permitió evitar una validación de formulario haciendo esto:<input type="button" onclick="angular.element(this).scope().edit.delete();" value="delete">
Purefan
24

La mejor explicación del concepto que he encontrado se encuentra aquí: https://groups.google.com/forum/#!msg/angular/kqFrwiysgpA/eB9mNbQzcHwJ

Para ahorrarte el clic:

// get Angular scope from the known DOM element
e = document.getElementById('myAngularApp');
scope = angular.element(e).scope();
// update the model with a wrap in $apply(fn) which will refresh the view for us
scope.$apply(function() {
    scope.controllerMethod(val);
}); 
Hombre sabio
fuente
14
Lo anterior funciona cuando la aplicación y el controlador coexisten en el mismo elemento. Para aplicaciones más complejas que utilizan una directiva ng-view para una plantilla, debe obtener el primer elemento dentro de la vista, no el elemento DOM de toda la aplicación. Tuve que buscar elementos con un document.getElementsByClassName ('ng-scope'); lista de nodos para descubrir el elemento DOM de alcance correcto para obtener.
goosemanjack
Sé que este es un hilo muy antiguo, pero creo que me estoy encontrando con este problema. ¿Alguien tiene algún código que muestre cómo recorrer la lista para descubrir el elemento DOM que tomar?
JerryKur
Ignora mi pregunta. Pude obtener este trabajo simplemente usando document.getElementById ('any-Control-That-An-NG-Directiva'). Scope ().
JerryKur
si utiliza ng-view y divide sus vistas en sus propios archivos. puede poner y iden el elemento HTML superior y luego hacer document.getElementById()esa identificación. Esto le da acceso al alcance de ese controlador. métodos / propiedades, etc ... simplemente poniendo un punto fino en el comentario de goosemanjack.
ftravers
13

Más allá de las otras respuestas. Si no desea acceder a un método en un controlador pero desea acceder al servicio directamente, puede hacer algo como esto:

// Angular code* :
var myService = function(){
    this.my_number = 9;
}
angular.module('myApp').service('myService', myService);


// External Legacy Code:
var external_access_to_my_service = angular.element('body').injector().get('myService');
var my_number = external_access_to_my_service.my_number 
Alec Hewitt
fuente
13

Gracias a la publicación anterior, puedo actualizar mi modelo con un evento asincrónico.

<div id="control-panel" ng-controller="Filters">
    <ul>
        <li ng-repeat="filter in filters">
        <button type="submit" value="" class="filter_btn">{{filter.name}}</button>
        </li>
    </ul>
</div>

Declaro mi modelo

function Filters($scope) {
    $scope.filters = [];
}

Y actualizo mi modelo desde fuera de mi alcance

ws.onmessage = function (evt) {
    dictt = JSON.parse(evt.data);
    angular.element(document.getElementById('control-panel')).scope().$apply(function(scope){
        scope.filters = dictt.filters;
    });
};
Guillaume Vincent
fuente
6

Una forma más segura y eficaz, especialmente cuando los datos de depuración están desactivados, es usar una variable compartida para mantener una función de devolución de llamada. Su controlador angular implementa esta función para devolver sus componentes internos al código externo.

var sharedVar = {}
myModule.constant('mySharedVar', sharedVar)
mymodule.controller('MyCtrl', [ '$scope','mySharedVar', function( $scope, mySharedVar) {

var scopeToReturn = $scope;

$scope.$on('$destroy', function() {
        scopeToReturn = null;
    });

mySharedVar.accessScope = function() {
    return scopeToReturn;
}
}]);

Generalizado como directiva reutilizable:

Creé una directiva 'exposeScope' que funciona de manera similar pero el uso es más simple:

<div ng-controller="myController" expose-scope="aVariableNameForThisScope">
   <span expose-scope='anotherVariableNameForTheSameScope" />
</div>

Esto almacena el alcance actual (que se le da a la función de enlace de la directiva) en un objeto global 'scopes' que es el titular de todos los ámbitos. El valor proporcionado al atributo de directiva se usa como el nombre de propiedad del ámbito en este objeto global.

Vea la demostración aquí . Como mostré en la demostración, puede activar eventos jQuery cuando el ámbito se almacena y se elimina del objeto global 'ámbitos'.

<script type="text/javascript" >
    $('div').on('scopeLinked', function(e, scopeName, scope, allScopes) {
      // access the scope variable or the given name or the global scopes object
    }.on('scopeDestroyed', function(e, scopeName, scope, allScopes) {
      // access the scope variable or the given name or the global scopes object
    }

</script>

Tenga en cuenta que no he probado el encendido ('scopeDestroyed') cuando el elemento real se elimina del DOM. Si no funciona, puede ser útil activar el evento en el documento en lugar del elemento. (vea el script app.js) en el demo plunker.

Cagatay Kalan
fuente
3

Sé que esta es una vieja pregunta, pero estaba buscando opciones para hacer esto recientemente, así que pensé en poner mis hallazgos aquí en caso de que sea útil para alguien.

En la mayoría de los casos, si existe la necesidad de un código heredado externo para interactuar con el estado de la interfaz de usuario o el funcionamiento interno de la aplicación, un servicio podría ser útil para abstraer esos cambios. Si un código externo interactúa directamente con su controlador angular, componente o directiva, está acoplando su aplicación en gran medida con su código heredado, lo cual es una mala noticia.

Lo que terminé usando en mi caso, es una combinación de globales accesibles para el navegador (es decir, ventana) y manejo de eventos. Mi código tiene un motor de generación de formularios inteligente que requiere la salida JSON de un CMS para iniciar los formularios. Esto es lo que he hecho:

function FormSchemaService(DOM) {
    var conf = DOM.conf;

    // This event is the point of integration from Legacy Code 
    DOM.addEventListener('register-schema', function (e) {

       registerSchema(DOM.conf); 
    }, false);

    // service logic continues ....

El servicio de esquema de formulario se crea utilizando un inyector angular como se esperaba:

angular.module('myApp.services').
service('FormSchemaService', ['$window' , FormSchemaService ])

Y en mis controladores: function () {'use estricto';

angular.module('myApp').controller('MyController', MyController);

MyEncapsulatorController.$inject = ['$scope', 'FormSchemaService'];

function MyController($scope, formSchemaService) {
    // using the already configured formSchemaService
    formSchemaService.buildForm(); 

Hasta ahora, esta es una programación puramente angular y orientada al servicio de JavaScript. Pero la integración heredada viene aquí:

<script type="text/javascript">

   (function(app){
        var conf = app.conf = {
       'fields': {
          'field1: { // field configuration }
        }
     } ; 

     app.dispatchEvent(new Event('register-schema'));

 })(window);
</script>

Obviamente, cada enfoque tiene sus méritos e inconvenientes. Las ventajas y el uso de este enfoque dependen de su interfaz de usuario. Los enfoques sugeridos anteriormente no funcionan en mi caso, ya que mi esquema de formulario y mi código heredado no tienen control ni conocimiento de los ámbitos angulares. Por lo tanto, configurar mi aplicación en función angular.element('element-X').scope(); podría romper la aplicación si cambiamos los ámbitos. Pero si su aplicación tiene conocimiento del alcance y puede confiar en que no cambiará con frecuencia, lo que se sugiere anteriormente es un enfoque viable.

Espero que esto ayude. Cualquier comentario también es bienvenido.

Shakus
fuente