Rufen Sie AngularJS über Legacy-Code auf

180

Ich verwende AngularJS, um HTML-Steuerelemente zu erstellen, die mit einer älteren Flex-Anwendung interagieren. Alle Rückrufe von der Flex-App müssen an das DOM-Fenster angehängt werden.

Zum Beispiel (in AS3)

ExternalInterface.call("save", data);

Werde anrufen

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

Innerhalb der JS-Größenänderungsfunktion möchte ich ein Ereignis auslösen, das ein Controller hören kann. Es scheint, dass die Erstellung eines Dienstes der richtige Weg ist. Können Sie einen Dienst von außerhalb von AngularJS aktualisieren? Kann ein Controller auf Ereignisse von einem Dienst warten? In einem Experiment (Klicken für Geige) habe ich anscheinend auf einen Dienst zugreifen können, aber die Aktualisierung der Dienstdaten wird nicht in der Ansicht wiedergegeben (im Beispiel <option>sollte ein zum hinzugefügt werden <select>).

Vielen Dank!

Kreek
quelle
1
Beachten Sie, dass in der jsfiddle oben der Injektor erhalten wird, ohne ein Element in der App mit anzugreifen var injector = angular.injector(['ng', 'MyApp']);. Auf diese Weise erhalten Sie einen völlig neuen Kontext und ein Duplikat myService. Das bedeutet, dass Sie am Ende zwei Instanzen des Dienstes und des Modells haben und Daten an der falschen Stelle hinzufügen. Sie sollten stattdessen ein Element in der App mit auswählen angular.element('#ng-app').injector(['ng', 'MyApp']). An diesem Punkt können Sie dann $ apply verwenden, um Modelländerungen zu verpacken.
Thanh Nguyen

Antworten:

293

Interop von außerhalb von Angular zu Angular entspricht dem Debuggen von Angular-Anwendungen oder der Integration in eine Bibliothek eines Drittanbieters.

Für jedes DOM-Element können Sie Folgendes tun:

  • angular.element(domElement).scope() um den aktuellen Bereich für das Element abzurufen
  • angular.element(domElement).injector() um den aktuellen App-Injektor zu erhalten
  • angular.element(domElement).controller()um die ng-controllerInstanz zu erreichen.

Über den Injektor können Sie jeden Service in Winkelanwendung in Anspruch nehmen. Ebenso können Sie aus dem Bereich heraus alle Methoden aufrufen, die für ihn veröffentlicht wurden.

Beachten Sie, dass Änderungen am Winkelmodell oder Methodenaufrufe im Bereich folgendermaßen vorgenommen werden müssen $apply():

$scope.$apply(function(){
  // perform any model changes or method invocations here on angular app.
});
Misko Hevery
quelle
1
Das funktioniert, aber ich wünschte, es gäbe eine Möglichkeit, von einem Modul direkt zu seinem Umfang zu gelangen - ist das möglich? Wenn ich zurückgehen und eine Auswahl [ng-app]
treffen muss,
5
Ich kann das nicht zum Laufen bringen: Ich rufe an angular.element(document.getElementById(divName)).scope(), aber ich kann keine Funktionen davon aufrufen, es gibt nur "undefined" in der Konsole zurück.
Emil
1
Sogar ich habe das gleiche Problem wie oben von @Emil beschrieben, es kehrt undefiniert zurück. Irgendeine Hilfe ?
Bibin
5
element (). scope () funktioniert nicht, wenn die Debug-Daten deaktiviert sind. Dies ist die Empfehlung für die Produktion. Macht es das in diesem Szenario nicht nutzlos? Dies dient nur zum Testen / Debuggen.
K. Norbert
4
In Angular 1.3 möchten Sie dies nicht tun. Das Angular-Team wollte nicht, dass wir ".scope ()" für Elemente im Produktionscode aufrufen. Es sollte ein Debug-Tool sein. Ab Angular 1.3 können Sie dies also deaktivieren. Angular beendet das Anhängen des Bereichs an das Element mithilfe der .data-Funktion von jQuery. Dies beschleunigt Ihre App. Darüber hinaus führt die Übergabe Ihrer Bereiche an die Caching-Funktionen von jquery zu Speicherlecks. Sie sollten dies also auf jeden Fall deaktivieren, um Ihre App zu beschleunigen. Auf der Website von Angular finden Sie eine Produktionsanleitung, anhand derer Sie mehr erfahren sollten.
Frosty
86

Misko gab die richtige Antwort (offensichtlich), aber einige von uns Neulingen müssen sie möglicherweise weiter vereinfachen.

Wenn Sie AngularJS-Code aus Legacy-Apps aufrufen möchten, stellen Sie sich den AngularJS-Code als "Mikro-App" vor, die in einem geschützten Container in Ihrer Legacy-Anwendung vorhanden ist. Sie können es nicht direkt anrufen (aus gutem Grund), aber Sie können Remote-Aufrufe über das $ scope-Objekt tätigen.

Um das $ scope-Objekt zu verwenden, müssen Sie das Handle von $ scope abrufen. Zum Glück ist das sehr einfach.

Sie können die ID eines beliebigen HTML-Elements in Ihrem AngularJS-HTML-Code "micro-app" verwenden, um das Handle des AngularJS-Anwendungsbereichs $ abzurufen.

Angenommen, wir möchten einige Funktionen in unserem AngularJS-Controller aufrufen, z. B. sayHi () und sayBye (). Im AngularJS HTML (Ansicht) haben wir ein Div mit der ID "MySuperAwesomeApp". Sie können den folgenden Code in Kombination mit jQuery verwenden, um das Handle von $ scope zu erhalten:

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

Jetzt können Sie Ihre AngularJS-Codefunktionen über das Bereichshandle aufrufen:

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

microappscope.sayHi();

microappscope.sayBye();

Um die Arbeit zu vereinfachen, können Sie eine Funktion verwenden, mit der Sie jederzeit auf den Bereich zugreifen können, wenn Sie darauf zugreifen möchten:

function microappscope(){

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

}

Ihre Anrufe würden dann so aussehen:

microappscope().sayHi();

microappscope().sayBye();

Ein funktionierendes Beispiel finden Sie hier:

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

Ich habe dies auch in einer Diashow für die Ottawa AngularJS-Gruppe gezeigt (einfach zu den letzten beiden Folien springen)

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

Peter Drinnan
quelle
4
Beachten Sie, dass von Antworten nur auf Links abgeraten wird. SO-Antworten sollten der Endpunkt einer Suche nach einer Lösung sein (im Gegensatz zu einem weiteren Zwischenstopp von Referenzen, die im Laufe der Zeit veralten). Bitte fügen Sie hier eine eigenständige Zusammenfassung hinzu, wobei Sie den Link als Referenz behalten.
Kleopatra
Schöne zusätzliche Klarstellung. Vielen Dank.
Joe
1
Schöne Erklärung! erlaubte mir, eine Formularvalidierung zu umgehen, indem ich dies tat:<input type="button" onclick="angular.element(this).scope().edit.delete();" value="delete">
Purefan
24

Die beste Erklärung für das Konzept, das ich gefunden habe, finden Sie hier: https://groups.google.com/forum/#!msg/angular/kqFrwiysgpA/eB9mNbQzcHwJ

Um Ihnen das Klicken zu ersparen:

// 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);
}); 
Weiser Mann
quelle
14
Das Obige funktioniert, wenn App und Controller im selben Element vorhanden sind. Bei komplexeren Apps, die eine ng-view-Direktive für eine Vorlage verwenden, müssen Sie das erste Element in der Ansicht abrufen, nicht das DOM-Element der gesamten App. Ich musste Elemente mit einem document.getElementsByClassName ('ng-scope') durchsuchen. Knotenliste, um das richtige zu erfassende DOM-Element für den Bereich herauszufinden.
Goosemanjack
Ich weiß, dass dies ein wirklich alter Thread ist, aber ich denke, ich stoße auf dieses Problem. Hat jemand einen Code, der zeigt, wie man durch die Liste geht, um das zu greifende DOM-Element herauszufinden?
JerryKur
Ignoriere meine Frage. Ich konnte diese Arbeit nur mit document.getElementById ('any-Control-That-Has-An-NG-Directive'). Scope () erhalten.
JerryKur
Wenn Sie ng-view verwenden und Ihre Ansichten in eigene Dateien aufteilen. Sie können diese ID idin das oberste HTML-Element einfügen und dann document.getElementById()eingeben. Dadurch erhalten Sie Zugriff auf den Bereich dieses Controllers. Methoden / Eigenschaften etc ... nur einen feinen Punkt auf den Kommentar von goosemanjack setzen.
ftravers
13

Weiter zu den anderen Antworten. Wenn Sie nicht auf eine Methode in einem Controller zugreifen möchten, sondern direkt auf den Dienst zugreifen möchten, können Sie Folgendes tun:

// 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
quelle
13

Dank des vorherigen Beitrags kann ich mein Modell mit einem asynchronen Ereignis aktualisieren.

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

Ich erkläre mein Modell

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

Und ich aktualisiere mein Modell von außerhalb meines Anwendungsbereichs

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

Eine sicherere und performantere Methode, insbesondere wenn die Debug-Daten deaktiviert sind, besteht darin, eine gemeinsam genutzte Variable zum Halten einer Rückruffunktion zu verwenden. Ihr Winkelregler implementiert diese Funktion, um seine Interna an den externen Code zurückzugeben.

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

Verallgemeinert als wiederverwendbare Richtlinie:

Ich habe eine 'exposeScope'-Direktive erstellt, die auf ähnliche Weise funktioniert, deren Verwendung jedoch einfacher ist:

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

Dadurch wird der aktuelle Bereich (der für die Verknüpfungsfunktion der Direktive angegeben ist) in einem globalen 'Scopes'-Objekt gespeichert, das für alle Bereiche ein Halter ist. Der für das Direktivenattribut angegebene Wert wird als Eigenschaftsname des Bereichs in diesem globalen Objekt verwendet.

Sehen Sie die Demo hier . Wie ich in der Demo gezeigt habe, können Sie jQuery-Ereignisse auslösen, wenn der Bereich gespeichert und aus dem globalen Bereich 'Bereiche' entfernt wird.

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

Beachten Sie, dass ich das on ('scopeDestroyed') nicht getestet habe, wenn das eigentliche Element aus dem DOM entfernt wurde. Wenn dies nicht funktioniert, kann es hilfreich sein, das Ereignis im Dokument selbst anstelle des Elements auszulösen. (siehe app.js) Skript im Demo-Plunker.

Cagatay Kalan
quelle
3

Ich weiß, dass dies eine alte Frage ist, aber ich habe kürzlich nach Optionen gesucht, um dies zu tun. Deshalb dachte ich, ich habe meine Ergebnisse hier veröffentlicht, falls es für jemanden nützlich ist.

In den meisten Fällen kann ein Dienst hilfreich sein, um diese Änderungen zu abstrahieren, wenn externer Legacy-Code für die Interaktion mit dem Status der Benutzeroberfläche oder dem Innenleben der Anwendung erforderlich ist. Wenn ein externer Code direkt mit Ihrem Winkelcontroller, Ihrer Komponente oder Ihrer Direktive interagiert, koppeln Sie Ihre App stark mit Ihrem Legacy-Code, was eine schlechte Nachricht ist.

Was ich in meinem Fall letztendlich verwendet habe, ist eine Kombination aus über den Browser zugänglichen Globals (dh Fenster) und Ereignisbehandlung. Mein Code verfügt über eine intelligente Formulargenerierungs-Engine, die eine JSON-Ausgabe von einem CMS erfordert, um die Formulare zu initiieren. Folgendes habe ich getan:

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

Der Formularschemadienst wird erwartungsgemäß mit einem Winkelinjektor erstellt:

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

Und in meinen Controllern: function () {'use strict';

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

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

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

Bisher ist dies eine reine eckige und Javascript-serviceorientierte Programmierung. Aber die Legacy-Integration kommt hierher:

<script type="text/javascript">

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

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

 })(window);
</script>

Offensichtlich hat jeder Ansatz seine Vor- und Nachteile. Die Vorteile und die Verwendung dieses Ansatzes hängen von Ihrer Benutzeroberfläche ab. Die zuvor vorgeschlagenen Ansätze funktionieren in meinem Fall nicht, da mein Formularschema und mein Legacy-Code keine Kontrolle und keine Kenntnis der Winkelbereiche haben. Daher kann die Konfiguration meiner App basierend auf angular.element('element-X').scope(); möglicherweise die App beschädigen, wenn wir die Bereiche ändern. Wenn Ihre App jedoch Kenntnisse über den Umfang hat und sich darauf verlassen kann, dass sich diese nicht häufig ändern, wird zuvor ein praktikabler Ansatz vorgeschlagen.

Hoffe das hilft. Jedes Feedback ist ebenfalls willkommen.

Shakus
quelle