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

59,105

Solution 1

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

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.

Solution 2

$watch object is available inside $scope object, so you need to add $scope inside your controller factory function & then place watcher over the variable.

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

Demo Here

Note: I know you had converted directive to component, to remove dependency of $scope so that you will get one step closer to Angular2. But it seems like it didn't get removed for this case.

Update

Basically angular 1.5 does added .component method jus differentiate two different functionality. Like component.stands to perform particular behaviby adding selector, where as directive stands to add specific behavior to DOM. Directive is just wrapper method on .directive DDO(Directive Definition Object). Only what you can see is, they had remove link/compile function while using .component method where you had an ability to get angular compiled DOM.

Do use $onChanges/$doCheck lifecycle hook of Angular component lifecycle hook, those will be available after Angular 1.5.3+ version.

$onChanges(changesObj) - Called whenever bindings are updated. The changesObj is a hash whose keys are the names of the bound properties.

$doCheck() - Called on each turn of the digest cycle when binding changes. Provides an opportunity to detect and act on changes.

By using same function inside component will ensure your code to be compatible to move to Angular 2.

Solution 3

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

Solution 4

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>
Share:
59,105

Related videos on Youtube

Ka Tech
Author by

Ka Tech

Updated on July 05, 2022

Comments

  • Ka Tech
    Ka Tech almost 2 years

    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 changes on an object? For a basic example I have custom component myBox which has a child component game with a binding on the game . If there is a change game within the game component how do I show an alert message within the myBox? I understand there is rxJS method is it possible to do this purely in angular? My 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
    Ka Tech over 8 years
    Thanks for this, I guess will have stick to this now. Are you able to point me to any links that discuss what alt methods available to observe obj state changes without using watch in preparation for AngularJs2?
  • Pankaj Parkar
    Pankaj Parkar over 8 years
    @KaTech now I can only say use observable of RxJS, that would be good compatible with Angular2
  • Ka Tech
    Ka Tech over 8 years
    Thanks I'm looking to react to the change of myBox.game rather than game.gameChange. As this is not an input but label, the above may not work. I think I may have to resort to rxjs in the end...
  • georgeawg
    georgeawg over 8 years
    I added info for propogating events to parent components.
  • Pankaj Parkar
    Pankaj Parkar over 8 years
    how this thing will handle up programmatic value change on myBox.game variable?
  • Mark Pieszak - Trilon.io
    Mark Pieszak - Trilon.io over 8 years
    Great answer @georgeawg. 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...
  • georgeawg
    georgeawg over 8 years
    @MarkPieszak You have a good question. Ask it with a specific example and see what answers you get.
  • Xavi
    Xavi about 8 years
    @KaTech @georgeawg try to avoid bindings with =. To avoid mutation of the parent scope is a better practice to use > and clone it every time it changes using the $onChanges method. via: Official docs
  • georgeawg
    georgeawg almost 8 years
    @MarkPieszak See updated answer with example of a reactive service.
  • Manuel Ferreiro
    Manuel Ferreiro over 7 years
    Good answer, only say that you can improve your RxJS solution using rx.BehaviorSubject() instead of rx.Subject(), which do the job of storing the last value by itself, and can be initialized with a default value just like you did in your service
  • georgeawg
    georgeawg almost 6 years
    With Angular 2+ the event bus is going away (event busses have performance issues.) To make the migration to Angular2+ easier, avoid using $scope and $rootScope. Consider using RxJS instead for event emitters and subscribers.
  • georgeawg
    georgeawg almost 6 years
    To make the migration to Angular2+ easier, avoid using $scope and $rootScope. Consider using RxJS instead for event emitters and subscribers.
  • mix3d
    mix3d over 5 years
    can you explain why this works? Isn't abusing rootscope a bad idea?