Changing Directive Inputs Programmatically Won't Trigger ngOnChanges In AngularJS 2 Beta 9
In Angular 2, when you set directive input bindings using the "[value]" property syntax, the ngOnChanges life-cycle method will be called once when the input value is initialized and then once for each subsequent change. As I just discovered, however, the ngOnChanges life-cycle method is only invoked when the changes are driven by the template syntax. If, on the other hand, you change the directive input value programmatically, the ngOnChanges life-cycle method will not be invoked.
Run this demo in my JavaScript Demos project on GitHub.
To demonstrate this, all we have to do is set up a simple component that exposes an input. Then, we can try to change that input programmatically and check to see if the component's ngOnChanges life-cycle method is called. In this case, I'll use a simple Counter component that renders a "value" input.
In the following code, notice that I am using two different means to set the Counter's value input. First, I'm using the template syntax to set the initial value to zero. Then, I'm using setInterval() so update the Counter's value input programmatically. This way, we can see how the two different approaches affect the ngOnChanges life-cycle method.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Changing Inputs Programmatically Won't Trigger ngOnChanges In AngularJS 2 Beta 9
</title>
</head>
<body>
<h1>
Changing Inputs Programmatically Won't Trigger ngOnChanges In AngularJS 2 Beta 9
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
requirejs(
[ /* Using require() for better readability. */ ],
function run() {
ng.platform.browser.bootstrap( require( "App" ) );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
define(
"App",
function registerApp() {
// Define the App component metadata.
ng.core
.Component({
selector: "my-app",
directives: [ require( "Counter" ) ],
// Let's configure a live query for the Counter component so that
// we can change the [value] programmatically.
queries: {
"counter": new ng.core.ViewChild( require( "Counter" ) )
},
// In this template, notice that we are binding a static value
// to the [value] property using the template syntax. Then, we
// are going to continue to update the value programmatically.
template:
`
<counter [value]="0"></counter>
`
})
.Class({
constructor: AppController,
// Define the life-cycle methods on the prototype so that they
// are picked up an run-time.
ngAfterViewInit: function noop() {}
})
;
return( AppController );
// I control the App component.
function AppController() {
var vm = this;
// I hold the value that will be piped into the Counter input.
var counterValue = 0;
// Expose the public methods.
vm.ngAfterViewInit = ngAfterViewInit;
// ---
// PUBLIC METHODS.
// ---
// I get called after the view has been initialized and the live
// queries have been bound.
function ngAfterViewInit() {
// Now that we have an injected reference to the Counter
// component instance, lets set up an interval to start
// incrementing the [value] property.
// --
// CAUTION: We can't set the initial value directly inside the
// ngAfterViewInit() method or we'll run into a change-detection
// error in which the View is changed as a side-effect of the
// view-init event. As such, we have to wrap any change inside
// some sort of timeout / interval.
setInterval(
function updateCounter() {
vm.counter.value = ++counterValue;
},
1000
);
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a counter that outputs the bound value.
define(
"Counter",
function registerCounter() {
// Define the Counter component metadata and return the constructor.
return ng.core
.Component({
selector: "counter",
inputs: [ "value" ],
template:
`
<strong>Current Count:</strong> {{ value }}
`
})
.Class({
// Leaving the constructor as a no-op since it doesn't have to
// do anything.
constructor: function noop() {},
// I get called whenever the bound inputs change.
ngOnChanges: function( event ) {
// Here, we are simply going to output every input change
// and determine whether or not it is the first change, or
// some subsequent change.
console.log(
"ngOnChanges [first]:",
event.value.isFirstChange(),
"-",
event.value.currentValue
);
}
})
;
}
);
</script>
</body>
</html>
Now, you might be thinking that the use of a static value in the template is affecting the ngOnChanges life-cycle. But, don't worry, it is not. If we were to remove the "[value]" binding altogether, we'd get the same result, only without an initial value. And, when we run the above code, we get the following page output:
As you can see, the ngOnChanges directive life-cycle method is invoked for the initial binding defined by the "[value]" property syntax. But, it is never called when we update the value programmatically within the setInterval() method.
While you're likely to use the property syntax most of the time when setting up an input binding, this is a really important detail to understand when you start creating custom inputs. Because, while a naked component might use something like a "[value]" binding, the ngModel's valueAccessor proxy will have to change the input programmatically. And, at that point, the underlying component's ngOnChanges life-cycle method may not work in the way you expected.
Want to use code from this post? Check out the license.
Reader Comments
Have you tried using/changing actual property of the parent programmatically? I mean instead of this:
counter [value]="0"
do this
counter [value]="someVarUpdatedProgrammatically"
@Yakov,
If you do that, then Yes, the ngOnChanges life-cycle method will continue to work as expected because you are using the property binding template syntax. And, in 99% of the cases, this is likely what we are doing. But, in a small set of edge-cases, you might not be able to / have access to the template itself and have to do things programmatically.
I plan to follow up with a few more posts on the topic to discuss the edge-cases.
@All,
This discovery has got me thinking about the implied contract of an Input property (as opposed to a vanilla public property). Some thoughts on the matter:
www.bennadel.com/blog/3055-public-properties-component-inputs-and-the-change-detection-contract-in-angular-2.htm
It's not something that's truly outlined in the docs; but, this is just my reasoning on the evidence.
@All,
I took a stab at trying to figure out how to trigger the ngOnChanges() life-cycle event manually when you are updating "Input" properties programmatically (such as in an ngModel proxy):
www.bennadel.com/blog/3057-manually-triggering-ngonchanges-inside-an-ngmodel-value-accessor-in-angular-2-beta-11.htm
It is completely non-trivial. I hope that I am waaaaay off here and that there is simpler solution.
So, I hit this issue as well. I was passing a new array into a component but that component's onChanges() lifecycle was not being triggered. However it wasn't the onChanges issue, it was that the setter for the input was not being called thus not triggering the onChanges for the component. It was unusual b/c the getter for the input was called and changes were reflected on the template by using {{inputValue}}, however the setter for the input was still not called.... This led me to some discoveries.
1) for every event involving the input (hover, click, ..), the getter of an input is called.. this does not mean a changeDetection cycle is preformed.
2) a changeDetection cycle only occurs in a component for events involving the component, so if you pass a value to some component it wont update until you mouseOver it or something.
3) (maybe same as 2) changeDetection does not go though the full component tree every cycle, only down to which component the event occurred in..
here, I created this simple component to help "see" when change detection occurs in a given component, really helps demystify CD. Just drop it into any component to see when an event triggers change detection in that component.
Solution... donno yet, if you find a good way to trigger CD programmaticly with ng2 let me know.
import { Component } from '@angular/core';
import template from './ChangeDetector.html';
@Component({
selector: 'change-detector',
template
})
export class ChangeDetector {
value : number = 0;
update() : number{
this.value += 1;
console.log("running change detection" + this.value);
return 0;
}
}
//template
<div hidden>
{{value}}
{{update()}}
</div>
Hello,
is there any progress? Any workaround? I am on 2.1.1 and I met with the same behavior. I tried to manually call detectChanges(), but it doesnt help.
@Jerrad,
if you reinit the array ngOnChange fires:
parent
setTimeout(() => { this.updateSomething(); }, 0);
}
updateSomething) {
this.someArray = [{name: 'somename'}]; // or []
}
child
ngOnChanges(changes: SimpleChanges) {
console.log('CHANGES ', changes);
}
@Ben,
onChange life cycle not working even if i change value of parent inside setInterval