AngularJS 1.5+ Komponenten unterstützen keine Watcher. Worum geht es?

78

Ich habe meine benutzerdefinierten Anweisungen auf die neue Komponentenarchitektur aktualisiert . Ich habe gelesen, dass Komponenten keine Beobachter unterstützen. Ist das richtig? Wenn ja, wie erkennen Sie Änderungen an einem Objekt? Als einfaches Beispiel habe ich eine benutzerdefinierte Komponente, myBoxdie ein untergeordnetes Komponentenspiel mit einer Bindung für das Spiel enthält. Wenn es in der Spielkomponente ein Änderungsspiel gibt, wie zeige ich eine Warnmeldung in der myBox an? Ich verstehe, dass es eine rxJS-Methode gibt. Ist es möglich, dies nur im Winkel zu tun? Meine JSFiddle

JavaScript

var app = angular.module('myApp', []);
app.controller('mainCtrl', function($scope) {

   $scope.name = "Tony Danza";

});

app.component("myBox",  {
      bindings: {},
      controller: function($element) {
        var myBox = this;
        myBox.game = 'World Of warcraft';
        //IF myBox.game changes, show alert message 'NAME CHANGE'
      },
      controllerAs: 'myBox',
      templateUrl: "/template",
      transclude: true
})
app.component("game",  {
      bindings: {game:'='},
      controller: function($element) {
        var game = this;


      },
      controllerAs: 'game',
      templateUrl: "/template2"
})

HTML

<div ng-app="myApp" ng-controller="mainCtrl">
  <script type="text/ng-template" id="/template">
    <div style='width:40%;border:2px solid black;background-color:yellow'>
      Your Favourite game is: {{myBox.game}}
      <game game='myBox.game'></game>
    </div>
  </script>

 <script type="text/ng-template" id="/template2">
    <div>
    </br>
        Change Game
      <textarea ng-model='game.game'></textarea>
    </div>
  </script>

  Hi {{name}}
  <my-box>

  </my-box>

</div><!--end app-->
Ka Tech
quelle

Antworten:

157

Komponenten ohne Beobachter schreiben

In dieser Antwort werden fünf Techniken beschrieben, mit denen AngularJS 1.5-Komponenten ohne Verwendung von Watchern geschrieben werden können.


Verwenden Sie die ng-changeRichtlinie

Welche alternativen Methoden stehen zur Verfügung, um Änderungen des Objektzustands zu beobachten, ohne watch zur Vorbereitung auf AngularJs2 zu verwenden?

Mit der ng-changeDirektive können Sie auf Eingabeänderungen reagieren.

<textarea ng-model='game.game' 
          ng-change="game.textChange(game.game)">
</textarea>

Um das Ereignis an eine übergeordnete Komponente weiterzugeben, muss der Ereignishandler als Attribut der untergeordneten Komponente hinzugefügt werden.

<game game='myBox.game' game-change='myBox.gameChange($value)'></game>

JS

app.component("game",  {
      bindings: {game:'=',
                 gameChange: '&'},
      controller: function() {
        var game = this;
        game.textChange = function (value) {
            game.gameChange({$value: value});
        });

      },
      controllerAs: 'game',
      templateUrl: "/template2"
});

Und in der übergeordneten Komponente:

myBox.gameChange = function(newValue) {
    console.log(newValue);
});

Dies ist die bevorzugte Methode für die Zukunft. Die AngularJS-Strategie $watchist nicht skalierbar, da es sich um eine Abfragestrategie handelt. Wenn die Anzahl der $watchHörer etwa 2000 erreicht, wird die Benutzeroberfläche träge. Die Strategie in Angular 2 besteht darin, das Framework reaktiver zu gestalten und das Anbringen $watchzu vermeiden $scope.


Verwenden Sie den $onChangesLebenszyklus-Haken

Mit Version 1.5.3 hat AngularJS $onChangesdem $compileService den Life-Cycle-Hook hinzugefügt .

Aus den Dokumenten:

Die Steuerung kann die folgenden Methoden bereitstellen, die als Lebenszyklus-Hooks fungieren:

  • $ onChanges (changesObj) - Wird aufgerufen, wenn Einweg- ( <) oder Interpolationsbindungen ( @) aktualisiert werden. Dies changesObjist ein Hash, dessen Schlüssel die Namen der gebundenen Eigenschaften sind, die sich geändert haben, und deren Werte ein Objekt des Formulars sind { currentValue: ..., previousValue: ... }. Verwenden Sie diesen Hook, um Aktualisierungen innerhalb einer Komponente auszulösen, z. B. das Klonen des gebundenen Werts, um eine versehentliche Mutation des äußeren Werts zu verhindern.

- AngularJS Comprehensive Directive API-Referenz - Life-Cycle-Hooks

Der $onChangesHaken wird verwendet, um auf externe Änderungen an der Komponente mit <Einwegbindungen zu reagieren . Die ng-changeDirektive wird verwendet, um Änderungen von der ng-modelSteuerung außerhalb der Komponente mit &Bindungen zu propagieren .


Verwenden Sie den $doCheckLebenszyklus-Haken

Mit Version 1.5.8 hat AngularJS $doCheckdem $compileService den Life-Cycle-Hook hinzugefügt .

Aus den Dokumenten:

Die Steuerung kann die folgenden Methoden bereitstellen, die als Lebenszyklus-Hooks fungieren:

  • $doCheck()- Wird bei jeder Umdrehung des Verdauungszyklus aufgerufen. Bietet die Möglichkeit, Änderungen zu erkennen und darauf zu reagieren. Alle Aktionen, die Sie als Reaktion auf die von Ihnen erkannten Änderungen ausführen möchten, müssen über diesen Hook aufgerufen werden. Die Implementierung hat keine Auswirkung darauf, wann $onChangesaufgerufen wird. Dieser Hook kann beispielsweise nützlich sein, wenn Sie eine gründliche Gleichheitsprüfung durchführen oder ein Datumsobjekt prüfen möchten, dessen Änderungen vom Änderungsdetektor von Angular nicht erkannt und somit nicht ausgelöst werden $onChanges. Dieser Hook wird ohne Argumente aufgerufen. Wenn Sie Änderungen erkennen, müssen Sie die vorherigen Werte zum Vergleich mit den aktuellen Werten speichern.

- AngularJS Comprehensive Directive API-Referenz - Life-Cycle-Hooks


Intercomponent Kommunikation mit require

Direktiven können erfordern, dass die Controller anderer Direktiven die Kommunikation untereinander ermöglichen. Dies kann in einer Komponente erreicht werden, indem eine Objektzuordnung für die Eigenschaft require bereitgestellt wird . Die Objektschlüssel geben die Eigenschaftsnamen an, unter denen die erforderlichen Controller (Objektwerte) an den Controller der erforderlichen Komponente gebunden werden.

app.component('myPane', {
  transclude: true,
  require: {
    tabsCtrl: '^myTabs'
  },
  bindings: {
    title: '@'
  },
  controller: function() {
    this.$onInit = function() {
      this.tabsCtrl.addPane(this);
      console.log(this);
    };
  },
  templateUrl: 'my-pane.html'
});

Weitere Informationen finden Sie im AngularJS Developer Guide - Intercomponent Communicatation


Push-Werte von einem Service mit RxJS

Was ist mit einer Situation, in der Sie beispielsweise einen Dienst haben, der den Status "Halten" hat? Wie kann ich Änderungen an diesem Service vornehmen, und andere zufällige Komponenten auf der Seite können sich einer solchen Änderung bewusst sein? Ich hatte in letzter Zeit Probleme, dieses Problem anzugehen

Erstellen Sie einen Dienst mit RxJS-Erweiterungen für Angular .

<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);

app.factory("DataService", function(rx) {
  var subject = new rx.Subject(); 
  var data = "Initial";

  return {
      set: function set(d){
        data = d;
        subject.onNext(d);
      },
      get: function get() {
        return data;
      },
      subscribe: function (o) {
         return subject.subscribe(o);
      }
  };
});

Dann abonnieren Sie einfach die Änderungen.

app.controller('displayCtrl', function(DataService) {
  var $ctrl = this;

  $ctrl.data = DataService.get();
  var subscription = DataService.subscribe(function onNext(d) {
      $ctrl.data = d;
  });

  this.$onDestroy = function() {
      subscription.dispose();
  };
});

Kunden können Änderungen mit abonnieren DataService.subscribeund Produzenten können Änderungen mit vorantreiben DataService.set.

Die DEMO auf PLNKR .

georgeawg
quelle
Danke, ich möchte eher auf die Änderung von myBox.game als auf game.gameChange reagieren. Da dies keine Eingabe, sondern eine Bezeichnung ist, funktioniert das oben Gesagte möglicherweise nicht. Ich denke, ich muss am Ende möglicherweise auf rxjs zurückgreifen ...
Ka Tech
Ich habe Informationen zum Weitergeben von Ereignissen an übergeordnete Komponenten hinzugefügt.
Georgeawg
Wie wird dieses Ding mit programmatischen Wertänderungen an myBox.gameVariablen umgehen ?
Pankaj Parkar
Tolle Antwort @georgeawg. Was ist in einer Situation , wo Sie haben , Servicedass der Staat zum Beispiel zu halten. Wie kann ich Änderungen an diesem Service vornehmen, und andere zufällige Komponenten auf der Seite können sich einer solchen Änderung bewusst sein?
Ich hatte in
1
Gute Antwort, sagen Sie nur, dass Sie Ihre RxJS-Lösung mit rx.BehaviorSubject () anstelle von rx.Subject () verbessern können, das den letzten Wert selbst speichert und wie Sie mit einem Standardwert initialisiert werden kann in Ihrem Dienst
Manuel Ferreiro
8

$watchDas Objekt ist innerhalb des $scopeObjekts verfügbar. Sie müssen also $scopedie Factory-Funktion Ihres Controllers hinzufügen und dann den Watcher über der Variablen platzieren.

$scope.$watch(function(){
    return myBox.game;
}, function(newVal){
   alert('Value changed to '+ newVal)
});

Demo hier

Hinweis: Ich weiß , Sie konvertiert war , directiveum die componentAbhängigkeit von zu entfernen , $scopeso dass Sie einen Schritt näher an Angular2 bekommen. Aber es scheint, dass es für diesen Fall nicht entfernt wurde.

Aktualisieren

Grundsätzlich unterscheidet Winkel 1.5 die hinzugefügte .componentMethode, um zwei verschiedene Funktionen zu unterscheiden. Wie component.stands, um ein bestimmtes Verhalten hinzuzufügen selector, wobei as directivesteht, um DOM ein bestimmtes Verhalten hinzuzufügen. Die Direktive ist nur eine Wrapper-Methode für .directiveDDO (Directive Definition Object). Sie können nur sehen, dass sie eine Entfernungsfunktion hatten, link/compilewährend .componentSie eine Methode verwendeten, bei der Sie die Möglichkeit hatten, ein eckig kompiliertes DOM zu erhalten.

Verwenden Sie den $onChanges/ $doCheckLifecycle-Hook des Angular-Komponenten-Lifecycle-Hooks. Diese sind ab der Angular 1.5.3+ -Version verfügbar.

$ onChanges (changesObj) - Wird aufgerufen, wenn Bindungen aktualisiert werden. Der ChangesObj ist ein Hash, dessen Schlüssel die Namen der gebundenen Eigenschaften sind.

$ doCheck () - Wird bei jeder Runde des Digest-Zyklus aufgerufen, wenn sich die Bindung ändert. Bietet die Möglichkeit, Änderungen zu erkennen und darauf zu reagieren.

Durch die Verwendung derselben Funktion innerhalb der Komponente wird sichergestellt, dass Ihr Code kompatibel ist, um zu Angular 2 zu wechseln.

Pankaj Parkar
quelle
Danke dafür, ich denke, ich werde mich jetzt daran halten. Können Sie mich auf Links verweisen, in denen erläutert wird, welche alternativen Methoden verfügbar sind, um Änderungen des Objektstatus zu beobachten, ohne watch zur Vorbereitung auf AngularJs2 zu verwenden?
Ka Tech
1
@ KaTech jetzt kann ich nur sagen, Verwendung observablevon RxJS, das wäre gut kompatibel mitAngular2
Pankaj Parkar
4

Für alle, die an meiner Lösung interessiert sind, greife ich am Ende auf RXJS Observables zurück, die Sie verwenden müssen, wenn Sie zu Angular 2 gelangen. Hier ist eine funktionierende Geige für die Kommunikation zwischen Komponenten, mit der ich mehr Kontrolle darüber habe, was ich sehen soll.

JS FIDDLE RXJS Observables

class BoxCtrl {
    constructor(msgService) {
    this.msgService = msgService
    this.msg = ''

    this.subscription = msgService.subscribe((obj) => {
      console.log('Subscribed')
      this.msg = obj
    })
    }

  unsubscribe() {
    console.log('Unsubscribed')
    msgService.usubscribe(this.subscription)
  }
}

var app = angular
  .module('app', ['ngMaterial'])
  .controller('MainCtrl', ($scope, msgService) => {
    $scope.name = "Observer App Example";
    $scope.msg = 'Message';
    $scope.broadcast = function() {
      msgService.broadcast($scope.msg);
    }
  })
  .component("box", {
    bindings: {},
    controller: 'BoxCtrl',
    template: `Listener: </br>
    <strong>{{$ctrl.msg}}</strong></br>
    <md-button ng-click='$ctrl.unsubscribe()' class='md-warn'>Unsubscribe A</md-button>`
  })
  .factory('msgService', ['$http', function($http) {
    var subject$ = new Rx.ReplaySubject();
    return {
      subscribe: function(subscription) {
        return subject$.subscribe(subscription);
      },
      usubscribe: function(subscription) {
        subscription.dispose();
      },
      broadcast: function(msg) {
        console.log('success');
        subject$.onNext(msg);
      }
    }
  }])
Ka Tech
quelle
2

Ein kleines Heads-up zur Verwendung von ng-change, wie in der akzeptierten Antwort empfohlen, zusammen mit einer eckigen 1,5-Komponente.

Im Fall , dass Sie eine Komponente , die beobachten ng-modelund ng-changenicht arbeiten, können Sie die Parameter übergeben , wie:

Markup, in dem die Komponente verwendet wird:

<my-component on-change="$ctrl.doSth()"
              field-value="$ctrl.valueToWatch">
</my-component>

Komponente js:

angular
  .module('myComponent')
  .component('myComponent', {
    bindings: {
      onChange: '&',
      fieldValue: '='
    }
  });

Komponenten-Markup:

<select ng-model="$ctrl.fieldValue"
        ng-change="$ctrl.onChange()">
</select>
Wtower
quelle
0

Verfügbar in IE11, MutationObserver https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver . Sie müssen $ element service in den Controller einfügen, wodurch die Trennung von DOM und Controller halb unterbrochen wird, aber ich bin der Meinung, dass dies eine grundlegende Ausnahme (dh ein Fehler) bei Angularjs ist. Da hide / show asynchron ist, benötigen wir einen On-Show-Rückruf, den anglejs & angular-bootstrap-tab nicht bieten. Es erfordert auch, dass Sie wissen, welches spezifische DOM-Element Sie beobachten möchten. Ich habe den folgenden Code für den AngularJS-Controller verwendet, um den angezeigten Highcharts-Diagramm-Reflow auszulösen.

const myObserver = new MutationObserver(function (mutations) {
    const isVisible = $element.is(':visible') // Requires jquery
    if (!_.isEqual(isVisible, $element._prevIsVisible)) { // Lodash
        if (isVisible) {
            $scope.$broadcast('onReflowChart')
        }
        $element._prevIsVisible = isVisible
    }
})
myObserver.observe($element[0], {
    attributes: true,
    attributeFilter: ['class']
})
user982671
quelle
Vermeiden Sie die Verwendung von $scopeund, um die Migration zu Angular2 + zu vereinfachen $rootScope. Verwenden Sie stattdessen RxJS für Ereignisemitter und Abonnenten.
Georgeawg
0

Wirklich nette akzeptierte Antwort, aber ich könnte hinzufügen, dass Sie auch die Kraft von Ereignissen nutzen können (ein bisschen wie bei Qt-Signalen / Slots, wenn Sie so wollen).

Ein Ereignis wird gesendet: $rootScope.$broadcast("clickRow", rowId) von einem übergeordneten (oder sogar untergeordneten Controller). Dann können Sie in Ihrem Controller das Ereignis wie folgt behandeln:

$scope.$on("clickRow", function(event, data){
    // do a refresh of the view with data == rowId
});

Sie können auch eine solche Protokollierung hinzufügen (von hier aus: https://stackoverflow.com/a/34903433/3147071 )

var withLogEvent = true; // set to false to avoid events logs
app.config(function($provide) {
    if (withLogEvent)
    {
      $provide.decorator("$rootScope", function($delegate) {
        var Scope = $delegate.constructor;
        var origBroadcast = Scope.prototype.$broadcast;
        var origEmit = Scope.prototype.$emit;

        Scope.prototype.$broadcast = function() {
          console.log("$broadcast was called on $scope " + this.$id + " with arguments:",
                     arguments);
          return origBroadcast.apply(this, arguments);
        };
        Scope.prototype.$emit = function() {
          console.log("$emit was called on $scope " + this.$id + " with arguments:",
                     arguments);
          return origEmit.apply(this, arguments);
        };
        return $delegate;
      });
    }
});
Sebius
quelle
2
Mit Angular 2+ wird der Ereignisbus nicht mehr verwendet (Ereignisbusse weisen Leistungsprobleme auf). Um die Migration zu Angular2 + zu vereinfachen, vermeiden Sie die Verwendung von $scopeund $rootScope. Verwenden Sie stattdessen RxJS für Ereignisemitter und Abonnenten.
Georgeawg
0

Ich bin spät dran. Aber es kann anderen Menschen helfen.

app.component("headerComponent", {
    templateUrl: "templates/header/view.html",
    controller: ["$rootScope", function ($rootScope) {
        let $ctrl = this;
        $rootScope.$watch(() => {
            return $ctrl.val;
        }, function (newVal, oldVal) {
            // do something
        });
    }]
});
Tetra-Meister
quelle
Kannst du erklären, warum das funktioniert? Ist der Missbrauch von Rootscope nicht eine schlechte Idee?
mix3d