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

4

5

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);
    };
})
Lindeman answered 4/6, 2016 at 23:53 Comment(1)
I think this will be helpful to you as it was for me: toddmotto.com/angular-1-5-lifecycle-hooks#onchangesPaddlefish
V
4

It's $onChanges and not $onChange.

Also, the onChange only updates when the parent value is changed, not the child. Take a look at this plunkr. Note the console.log only fires when you type in the first input.

Vest answered 5/6, 2016 at 0:27 Comment(4)
Ah yah sorry typo, but if you use my original example and change it to $onChanges it doesn't work. I have it correct in my application, which doesn't work. You've changed the example to pass in a primitive. So does $onChanges only update on primitives passed into it and does not do a deep check if you pass in an object?Lindeman
Sorry missed that last part, so $onChanges is not for changes made to a child's bindings to allow you to tell the parent (if you want) without using the watch that comes with two-way binding? I don't understand, if the binding is from parent to child, why would a change in the parent not already be affecting the child, and $onChange would be used to allow and update to occur upwards.Lindeman
Wow, that seems counter intuitiveLindeman
I think the fact that it still updates is just a side effect of how JS objects work. From the documentation: 'Note however, that both parent and component scope reference the same object, so if you are changing object properties or array elements in the component, the parent will still reflect that change. The general rule should therefore be to never change an object or array property in the component scope.' docs.angularjs.org/guide/component explains it pretty well.Vest
S
10

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

Seer answered 12/8, 2016 at 19:40 Comment(0)
V
4

It's $onChanges and not $onChange.

Also, the onChange only updates when the parent value is changed, not the child. Take a look at this plunkr. Note the console.log only fires when you type in the first input.

Vest answered 5/6, 2016 at 0:27 Comment(4)
Ah yah sorry typo, but if you use my original example and change it to $onChanges it doesn't work. I have it correct in my application, which doesn't work. You've changed the example to pass in a primitive. So does $onChanges only update on primitives passed into it and does not do a deep check if you pass in an object?Lindeman
Sorry missed that last part, so $onChanges is not for changes made to a child's bindings to allow you to tell the parent (if you want) without using the watch that comes with two-way binding? I don't understand, if the binding is from parent to child, why would a change in the parent not already be affecting the child, and $onChange would be used to allow and update to occur upwards.Lindeman
Wow, that seems counter intuitiveLindeman
I think the fact that it still updates is just a side effect of how JS objects work. From the documentation: 'Note however, that both parent and component scope reference the same object, so if you are changing object properties or array elements in the component, the parent will still reflect that change. The general rule should therefore be to never change an object or array property in the component scope.' docs.angularjs.org/guide/component explains it pretty well.Vest
H
1

As others said above, Angular does not watch for changes in object properties, however, you can make Angular believe that your object is changed by reference.

It is sufficient to do a shallow copy of the object in order to trigger an $onChanges event:

vm.campaign = angular.extend({}, vm.campaign);

Credits to @gkalpak

Haruspicy answered 7/11, 2017 at 16:35 Comment(0)
G
0

Dealing with $onChanges is tricky. Actually, thats why in version 1.5.8 they introduced the $doCheck, similar to Angular 2 ngDoCheck.

This way, you can manually listen to changes inside the object being listened, which does not occur with the $onChanges hook (called only when the reference of the object is changed). Its the same thing, but it gets called for every digest cycle allowing you to check for changes manually (but better then watches).

For more details, see this blog post.

Guardant answered 7/2, 2017 at 22:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.