Earlier this week, I stumbled over the fact that Angular 2 won't trigger the ngOnChanges() life-cycle method if the component inputs are changed programmatically; this change detection integration seems to be tightly coupled to the property binding template syntax. This realization got me thinking about the difference between public properties and "Input properties;" and, the kind of contract that is assumed when exposing public properties as component Input properties. This "Input property" contract complicates the use of ngModel because ngModel acts as a proxy to those Input properties. This means that ngModel changes the proxied Input properties programmatically (via the value accessor bridge); which, in turn, means that the ngOnChangs() life-cycle method won't be triggered by Angular. So, in order to uphold the Input property contract, we have to figure out how to manually trigger the ngOnChanges() life-cycle event from within our ngModel value accessors.
CAUTION: Due to the extreme complexity of this situation, I have to assume that something in my thinking is just plain wrong. Perhaps Input properties don't really incur an unspoken contract? Perhaps you should never depend on the ngOnChanges() life-cycle event being invoked? Of course, neither of those thoughts feel "correct" either. All to say, take the following with a grain of salt. I think I might be off in no-mans-land.
When the ngOnChanges() life-cycle event is triggered, it is passed a collection of SimpleChange objects. Each of these objects has an instance method, isFirstChange(), which allows us to differentiate between input initialization and input update. If we are going to trigger the ngOnChanges() life-cycle method manually, then we have to make sure that our SimpleChange objects follow the same behavior.
The problem with this requirement is that, under the hood, the SimpleChange class is using an internal token to denote an uninitialized value:
Angular 2 Beta 11 has chosen not to export this value in any of its barrels (at least not that I could find); which means that when we set up our manually-created SimpleChange objects, we have no way to pass this internal token as the initial "previousValue" property of the SimpleChange constructor. To get around this, we have to track the first write (of the ngModel directive) and then explicitly override the isFirstChange() instance method during the first round of changes.
ASIDE: Like I cautioned above, this is so complex, one has to assume that it is the wrong approach.
To explore this problem, I've created a simple Toggle component that takes a "value" input and emits a "valueChange" output sequence. Then, I've proxied the value Input using the ngModel and provided an NG_VALUE_ACCESSOR bridge that manually triggers the ngOnChanges() when the following actions occur:
- The value is changed externally to the Toggle component.
- The valueChange is emitted by the Toggle component and needs to be piped back into the Toggle component as part of the two-way data-binding workflow.
The most interesting code is as the bottom:
There's a lot of code here, I know. I tried to walk through it in a meaningful way in the video. But, the result is that when we proxy the Toggle's value Input through the ngModel directive, our Toggle component can still depend on the ngOnChanges() life-cycle event. And, when we run this page and toggle the widget a few times, we get the following output:
I really hope that someone from the Angular team comes along and shows me how all of this crazy complexity can be reduced to one line of code that I missed somewhere. This just doesn't feel right. And it certainly doesn't feel elegant.
Want to use code from this post? Check out the license.