Public Properties, Component Inputs, And The Change-Detection Contract In Angular 2
Yesterday, I blogged about how the ngOnChanges() life-cycle method won't be triggered if component inputs are changed programmatically from an external source. This has got me thinking about the differences between the "public properties" and the "Input properties" of a component (ie, a directive) in Angular 2; and, more specifically, about the "contract" that is assumed by the developer when a public property is exposed as an Input property.
Public properties and Input properties are very similar in that they are both publicly accessible on the class that controls the given Angular 2 directive. But, I would argue that they are not the same. A public property is just a value. Nothing more, nothing less. When a public property is explicitly exposed as an Input property, however, it takes on additional responsibilities. It is no longer a simple data point; it is now an integral part of the selected change-detection strategy and the overall life-cycle of the given directive.
CAUTION: What follows is my personal reasoning on the matter - nothing below is stated in the Angular 2 documentation.
From my perspective, this means that when a developer exposes a public property as an Input property on an Angular 2 directive, the developer is buying into a few assumptions:
- Changes to the Input properties will always result in a call to the ngOnChanges() life-cycle method.
- Changes to the Input properties will always result in a change detection check (assuming that change detection is not somehow detached from the given directive).
Most of the time, you'll never have to think about this implicit contract since, most of the time, you'll be using component attributes to bind to component inputs. But, when you start getting into the world of custom user interface (UI) controls, this contract becomes a lot more relevant. Once you start using ngModel to manage the flow of data, how you propagate values begins to matter. If your ngModel valueAccessor changes an Input property on a target component, you need to adhere to the contract.
So, how do you actually do this? How do you make sure that change-detection runs and that the ngOnChanges() life-cycle method is invoked if you programmatically change an Input property (from an external source like an ngModel valueAccessor)? Honestly, at this time, I am not sure. I've spent a few hours trying to get it to work; but, so far, I don't have a valid solution. Hopefully more to come on the topic!
After noodling on this contract - which I still think is correct - I wanted to see if I could figure out how to trigger the ngOnChanges() life-cycle event method manually from with the "value accessor" of an ngModel proxy. I was finally able to "get it working":
.... but it is so complicated that I am seriously hoping that my approach is totally off-base and that there is a more correct way to do it.