AngularJS 1.5.x $onChanges Not Working with One-Way Binding Changes

mtpultz picture mtpultz · Jun 5, 2016 · Viewed 8.6k times · Source

I don't understand why $onChanges isn't kicked off when I change a bound primitive in an input. Can someone see what I've done wrong, and explain this in an uncomplicated way? I made a plunkr of a quick test application after I couldn't get it to work in my actual application either.

angular
.module('test', [])
.component('test', {

    template: '<child application="vm.application"></child>',
    controller: 'testCtrl as vm'
})
.controller('testCtrl', function() {

    var vm = this;  

    vm.$onInit = function () {

        vm.application = {
            data: {
                name: 'Test'
            }
        }
    };
})
.component('child', {

    template: '<input type="text" ng-model="vm.application.data.name">',
    bindings: {
        application: '<'
    },
    controller: 'childCtrl as vm'
})
.controller('childCtrl', function() {

    var vm = this;

    vm.$onChanges = function (changes) {
        console.log('CHANGED: ', changes);
    };
})

Answer

David Remie picture David Remie · Aug 12, 2016

The $onChanges method is not called for changes on subproperties of an object. Default changes to objects generally follow this sequence within a components lifetime:

  1. UNINITIALIZED_VALUE to undefined
  2. undefined to {} or { someAttribute: someValue, .. }
  3. ({..} to undefined if you delete the object in a parent scope)

In order to watch subproperties you could use the $doCheck method that was added in 1.5.8. It is called on every digest cycle and it takes no parameters. With great power comes great responsibility. In that method you would put logic that detects whether a certain value has been updated or not - the new value will already be updated in the controller's scope, you just need to find a way to determine if the value changed compared to the previously known value.

You could set a previousValueOfObjectAttribute variable on the controller before you start to expect changes to this specific attribute (e.g. when subcomponent B calls an output binding function in component A, based on which the target object - which is an input binding to B - in A changes). In cases where it is not predictable when the change is about to occur, you could make a copy of the specific atributes of interest after any change observed via the $doCheck method.

In my specific use case, I did not explicitly check between an old and new value, but I used a promise (store $q.defer().promise) with the intention that any change I would 'successfully' observe in the $doCheck method would resolve that promise. My controller then looked something like the following:

dn.$doCheck = function () {
  if (dn.waitForInputParam &&
      dn.waitForInputParam.promise.$$state.status === 0 &&
      dn.targetObject.targetAttribute !== false)
    dn.waitForInputParam.resolve(dn.targetObject.targetAttribute);
}

dn.listenToInputChange = function () {
  dn.waitForInputParam = $q.defer();
  dn.waitForInputParam.promise.then(dn.onInputParamChanged);
}

dn.onInputParamChanged = function (value) {
  // do stuff
  //

  // start listening again for input changes -- should be async to prevent infinite $digest loop
  setTimeout(dn.listenToInputChange, 1);
}

(w.r.t. promise.$$state.status, see this post).

For all other intents and purposes, watching changes to primitive data types, you should still use $onChanges. Reference: https://docs.angularjs.org/guide/component