Appeler AngularJS à partir du code hérité

180

J'utilise AngularJS pour créer des contrôles HTML qui interagissent avec une application Flex héritée. Tous les rappels de l'application Flex doivent être attachés à la fenêtre DOM.

Par exemple (en AS3)

ExternalInterface.call("save", data);

Appellera

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

À partir de la fonction de redimensionnement JS, j'aimerais envoyer un événement qu'un contrôleur peut entendre. Il semble que la création d'un service soit la voie à suivre. Pouvez-vous mettre à jour un service depuis l'extérieur d'AngularJS? Un contrôleur peut-il écouter les événements d'un service? Dans une expérience (cliquez pour violon) j'ai fait, il semble que je puisse accéder à un service, mais la mise à jour des données du service ne se reflète pas dans la vue (dans l'exemple, un <option>devrait être ajouté au <select>).

Merci!

Kreek
la source
1
Notez que dans le jsfiddle ci-dessus, l'injecteur est obtenu sans cibler un élément de l'application en utilisant var injector = angular.injector(['ng', 'MyApp']);. Cela vous donnera un tout nouveau contexte et un doublon myService. Cela signifie que vous vous retrouverez avec deux instances du service et du modèle et que vous ajouterez des données au mauvais endroit. Vous devez plutôt cibler un élément de l'application à l'aide de angular.element('#ng-app').injector(['ng', 'MyApp']). À ce stade, vous pouvez ensuite utiliser $ apply pour encapsuler les modifications de modèle.
Thanh Nguyen

Réponses:

293

L'interopérabilité de l'extérieur d'angle à angulaire est identique au débogage d'une application angulaire ou à l'intégration avec une bibliothèque tierce.

Pour tout élément DOM, vous pouvez le faire:

  • angular.element(domElement).scope() pour obtenir la portée actuelle de l'élément
  • angular.element(domElement).injector() pour obtenir l'injecteur d'application actuel
  • angular.element(domElement).controller()pour mettre la main sur l' ng-controllerinstance.

Depuis l'injecteur, vous pouvez accéder à n'importe quel service en application angulaire. De la même manière, à partir de la portée, vous pouvez invoquer toutes les méthodes qui y ont été publiées.

Gardez à l'esprit que toute modification du modèle angulaire ou toute invocation de méthode sur la portée doit être enveloppée $apply()comme suit:

$scope.$apply(function(){
  // perform any model changes or method invocations here on angular app.
});
Misko Hevery
la source
1
cela fonctionne, mais j'aurais aimé qu'il y ait un moyen de passer directement d'un module à sa portée - est-ce possible? Devoir revenir en arrière et sélectionner le [ng-app]nœud racine semble à l'envers alors que j'ai déjà une référence au module ...
mindplay.dk
5
Je ne peux pas faire fonctionner cela: j'appelle angular.element(document.getElementById(divName)).scope(), mais je ne suis pas en mesure d'invoquer des fonctions à partir de celui-ci, il renvoie simplement "undefined" dans la console.
Emil
1
Même si je suis confronté au même problème que celui décrit ci-dessus par @Emil, il retourne undefined. De l'aide ?
Bibin
5
element (). scope () ne fonctionnera pas si les données de débogage sont désactivées, ce qui est recommandé pour la production. Cela ne le rend-il pas inutile dans ce scénario? Cela ne servira qu'à tester / déboguer.
K. Norbert
4
Vous ne voudrez pas faire cela dans Angular 1.3. L'équipe Angular n'avait pas l'intention de nous faire appeler ".scope ()" sur des éléments du code de production. C'était censé être un outil de débogage. Donc, à partir de Angular 1.3, vous pouvez désactiver cette option. Angular arrêtera d'attacher la portée à l'élément en utilisant la fonction .data de jQuery. Cela accélérera votre application. De plus, confier vos étendues aux fonctionnalités de mise en cache de jquery créera des fuites de mémoire. Donc, vous devez absolument désactiver cette option pour accélérer votre application. Le site d'Angular a un guide de production que vous devriez utiliser pour en savoir plus.
frosty
86

Misko a donné la bonne réponse (évidemment), mais certains d'entre nous, les débutants, peuvent avoir besoin de la simplifier davantage.

Lorsqu'il s'agit d'appeler du code AngularJS à partir d'applications héritées, considérez le code AngularJS comme une «micro application» existant dans un conteneur protégé de votre application héritée. Vous ne pouvez pas lui faire d'appels directement (pour une très bonne raison), mais vous pouvez faire des appels à distance via l'objet $ scope.

Pour utiliser l'objet $ scope, vous devez obtenir le handle de $ scope. Heureusement, c'est très facile à faire.

Vous pouvez utiliser l'id de n'importe quel élément HTML dans votre HTML "micro-application" AngularJS pour obtenir le handle de l'application $ scope AngularJS.

À titre d'exemple, disons que nous voulons appeler quelques fonctions dans notre contrôleur AngularJS telles que sayHi () et sayBye (). Dans le HTML AngularJS (vue), nous avons un div avec l'id "MySuperAwesomeApp". Vous pouvez utiliser le code suivant, combiné avec jQuery pour obtenir le handle de $ scope:

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

Vous pouvez maintenant appeler vos fonctions de code AngularJS via le handle de portée:

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

microappscope.sayHi();

microappscope.sayBye();

Pour rendre les choses plus pratiques, vous pouvez utiliser une fonction pour saisir la poignée de la portée à chaque fois que vous souhaitez y accéder:

function microappscope(){

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

}

Vos appels ressembleraient alors à ceci:

microappscope().sayHi();

microappscope().sayBye();

Vous pouvez voir un exemple fonctionnel ici:

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

J'ai également montré cela dans un diaporama pour le groupe Ottawa AngularJS (passez simplement aux 2 dernières diapositives)

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

Peter Drinnan
la source
4
Notez que les réponses liées uniquement aux liens sont déconseillées, les réponses SO devraient être le point final de la recherche d'une solution (par rapport à une autre escale de références, qui ont tendance à devenir obsolètes avec le temps). Veuillez envisager d'ajouter un synopsis autonome ici, en gardant le lien comme référence.
kleopatra
Belle clarification supplémentaire. Merci.
Joe
1
Belle explication! m'a permis de contourner une validation de formulaire en faisant ceci:<input type="button" onclick="angular.element(this).scope().edit.delete();" value="delete">
Purefan
24

La plus grande explication du concept que j'ai trouvée se trouve ici: https://groups.google.com/forum/#!msg/angular/kqFrwiysgpA/eB9mNbQzcHwJ

Pour vous épargner le 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);
}); 
Homme sage
la source
14
Ce qui précède fonctionne lorsque l'application et le contrôleur coexistent dans le même élément. Pour les applications plus complexes qui utilisent une directive ng-view dans un modèle, vous devez obtenir le premier élément de la vue, et non l'élément DOM de l'ensemble de l'application. J'ai dû fouiller autour des éléments avec un document.getElementsByClassName ('ng-scope'); liste de nœuds pour déterminer l'élément DOM de portée correct à saisir.
goosemanjack
Je sais que c'est un fil très ancien, mais je pense que je rencontre ce problème. Quelqu'un a-t-il un code qui montre comment parcourir la liste pour déterminer l'élément DOM à saisir?
JerryKur
Ignorez ma question. J'ai pu obtenir ce travail en utilisant simplement document.getElementById ('any-Control-That-Has-An-NG-Directive'). Scope ().
JerryKur
si vous utilisez ng-view et divisez vos vues dans leurs propres fichiers. vous pouvez mettre et iddans l'élément HTML supérieur, puis faire document.getElementById()cet identifiant. Cela vous donne accès à la portée de ce contrôleur. méthodes / propriétés etc ... juste mettre un point fin sur le commentaire de goosemanjack.
ftravers
13

Suite aux autres réponses. Si vous ne souhaitez pas accéder à une méthode dans un contrôleur mais souhaitez accéder directement au service, vous pouvez faire quelque chose comme ceci:

// 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
la source
13

Grâce au post précédent, je peux mettre à jour mon modèle avec un événement asynchrone.

<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>

Je déclare mon modèle

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

Et je mets à jour mon modèle hors de ma portée

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
la source
6

Un moyen plus sûr et plus performant, en particulier lorsque les données de débogage sont désactivées, consiste à utiliser une variable partagée pour contenir une fonction de rappel. Votre contrôleur angulaire implémente cette fonction pour renvoyer ses composants internes au code externe.

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;
}
}]);

Généralisée en tant que directive réutilisable:

J'ai créé une directive 'exposeScope' qui fonctionne de la même manière mais l'utilisation est plus simple:

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

Cela stocke la portée actuelle (qui est donnée à la fonction de lien de la directive) dans un objet global «scopes» qui est un support pour toutes les portées. La valeur fournie à l'attribut de directive est utilisée comme nom de propriété de la portée dans cet objet global.

Voir la démo ici . Comme je l'ai montré dans la démo, vous pouvez déclencher des événements jQuery lorsque la portée est stockée et supprimée de l'objet global 'scopes'.

<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>

Notez que je n'ai pas testé le on ('scopeDestroyed') lorsque l'élément réel est supprimé du DOM. Si cela ne fonctionne pas, déclencher l'événement sur le document lui-même au lieu de l'élément peut aider. (voir le script app.js) dans le plunker de démonstration.

Cagatay Kalan
la source
3

Je sais que c'est une vieille question, mais je cherchais des options pour le faire récemment, alors j'ai pensé mettre mes conclusions ici au cas où cela serait utile à quiconque.

Dans la plupart des cas, s'il est nécessaire que du code hérité externe interagisse avec l'état de l'interface utilisateur ou le fonctionnement interne de l'application, un service peut être utile pour résumer ces modifications. Si un code externe interagit directement avec votre contrôleur angulaire, composant ou directive, vous associez fortement votre application à votre code hérité, ce qui est une mauvaise nouvelle.

Ce que j'ai fini par utiliser dans mon cas, c'est une combinaison de globaux accessibles par navigateur (ie fenêtre) et de gestion d'événements. Mon code a un moteur de génération de formulaires intelligent qui nécessite une sortie JSON d'un CMS pour initier les formulaires. Voici ce que j'ai fait:

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 ....

Le service de schéma de formulaire est créé à l'aide d'un injecteur angulaire comme prévu:

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

Et dans mes contrôleurs: function () {'use strict';

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

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

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

Jusqu'à présent, il s'agit d'une programmation orientée service angulaire et javascript pure. Mais l'intégration héritée vient ici:

<script type="text/javascript">

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

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

 })(window);
</script>

De toute évidence, chaque approche a ses avantages et ses inconvénients. Les avantages et l'utilisation de cette approche dépendent de votre interface utilisateur. Les approches suggérées précédemment ne fonctionnent pas dans mon cas car mon schéma de formulaire et mon code hérité n'ont aucun contrôle et aucune connaissance des portées angulaires. Par conséquent, la configuration de mon application en fonction de angular.element('element-X').scope(); pourrait potentiellement casser l'application si nous changeons les portées autour. Mais si votre application connaît la portée et peut compter sur elle pour ne pas changer souvent, ce qui a été suggéré précédemment est une approche viable.

J'espère que cela t'aides. Tout commentaire est également le bienvenu.

Shakus
la source