AngularJS 1.5+ Components do not support Watchers, what is the work around?

前端 未结 7 1138
广开言路
广开言路 2020-11-22 09:23

I\'ve been upgrading my custom directives to the new component architecture. I\'ve read that components do not support watchers. Is this correct? If so how do you detect cha

相关标签:
7条回答
  • 2020-11-22 09:37

    Writing Components without Watchers

    This answer outlines five techniques to use to write AngularJS 1.5 components without using watchers.

    • Use the ng-change Directive
    • Use the $onChanges Life-cycle Hook
    • Use the $doCheck Life-cycle Hook
    • Intercomponent Communication with require
    • Push Values from a Service with RxJS

    Use the ng-change Directive

    what alt methods available to observe obj state changes without using watch in preparation for AngularJs2?

    You can use the ng-change directive to react to input changes.

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

    And to propagate the event to a parent component, the event handler needs to be added as an attribute of the child component.

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

    And in the parent component:

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

    This is the preferred method going forward. The AngularJS strategy of using $watch is not scalable because it is a polling strategy. When the number of $watch listeners reaches around 2000, the UI gets sluggish. The strategy in Angular 2 is to make the framework more reactive and avoid placing $watch on $scope.


    Use the $onChanges Life-cycle Hook

    With version 1.5.3, AngularJS added the $onChanges life-cycle hook to the $compile service.

    From the Docs:

    The controller can provide the following methods that act as life-cycle hooks:

    • $onChanges(changesObj) - Called whenever one-way (<) or interpolation (@) bindings are updated. The changesObj is a hash whose keys are the names of the bound properties that have changed, and the values are an object of the form { currentValue: ..., previousValue: ... }. Use this hook to trigger updates within a component such as cloning the bound value to prevent accidental mutation of the outer value.

    — AngularJS Comprehensive Directive API Reference -- Life-cycle hooks

    The $onChanges hook is used to react to external changes into the component with < one-way bindings. The ng-change directive is used to propogate changes from the ng-model controller outside the component with & bindings.


    Use the $doCheck Life-cycle Hook

    With version 1.5.8, AngularJS added the $doCheck life-cycle hook to the $compile service.

    From the Docs:

    The controller can provide the following methods that act as life-cycle hooks:

    • $doCheck() - Called on each turn of the digest cycle. Provides an opportunity to detect and act on changes. Any actions that you wish to take in response to the changes that you detect must be invoked from this hook; implementing this has no effect on when $onChanges is called. For example, this hook could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not be detected by Angular's change detector and thus not trigger $onChanges. This hook is invoked with no arguments; if detecting changes, you must store the previous value(s) for comparison to the current values.

    — AngularJS Comprehensive Directive API Reference -- Life-cycle hooks


    Intercomponent Communication with require

    Directives can require the controllers of other directives to enable communication between each other. This can be achieved in a component by providing an object mapping for the require property. The object keys specify the property names under which the required controllers (object values) will be bound to the requiring component's controller.

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

    For more information, see AngularJS Developer Guide - Intercomponent Communicatation


    Push Values from a Service with RxJS

    What about in a situation where you have a Service that's holding state for example. How could I push changes to that Service, and other random components on the page be aware of such a change? Been struggling with tackling this problem lately

    Build a service with RxJS Extensions for 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);
          }
      };
    });
    

    Then simply subscribe to the changes.

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

    Clients can subscribe to changes with DataService.subscribe and producers can push changes with DataService.set.

    The DEMO on PLNKR.

    0 讨论(0)
  • 2020-11-22 09:38

    A small heads-up regarding the use of ng-change, as recommended with the accepted answer, together with an angular 1.5 component.

    In case you need to watch a component that ng-model and ng-change do not work, you can pass the parameters as:

    Markup in which component is used:

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

    Component js:

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

    Component markup:

    <select ng-model="$ctrl.fieldValue"
            ng-change="$ctrl.onChange()">
    </select>
    
    0 讨论(0)
  • 2020-11-22 09:38

    Available in IE11, MutationObserver https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver. You need to inject $element service into controller which semi-breaks DOM/controller separation, but I feel that this is a fundamental exception (ie. flaw) in angularjs. Since hide/show is async, we need on-show callback, that angularjs & angular-bootstrap-tab do not provide. It also requires that u know which specific DOM element u want to observe. I used following code for angularjs controller to trigger Highcharts chart reflow on-show.

    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']
    })
    
    0 讨论(0)
  • 2020-11-22 09:46

    I'm late. But it can help to another people.

    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
            });
        }]
    });
    
    0 讨论(0)
  • 2020-11-22 09:53

    To anyone interested in my solution, I end up resorting to RXJS Observables, which what you will have to use when you get to Angular 2. Here is a working fiddle for communications between components, it gives me more control on what to watch.

    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);
          }
        }
      }])
    
    0 讨论(0)
  • 2020-11-22 10:00

    Really Nice accepted answer, but I might add that you can use also the power of events ( a bit like in Qt signal / slots if you will ).

    An event is broadcast : $rootScope.$broadcast("clickRow", rowId) by any parent ( or even children controller ). Then in your controller you can handle the event like this :

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

    You can also add some logging on that like this ( taken from here : 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;
          });
        }
    });
    
    0 讨论(0)
提交回复
热议问题