Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the New York ColdFusion User Group (May. 2009) with: Gert Franz and Peter Bell and Mark Drew
Ben Nadel at the New York ColdFusion User Group (May. 2009) with: Gert Franz@gert_railo ) , Peter Bell@peterbell ) , and Mark Drew@markdrew )

Differentiating Between Initialization And Update With ngOnChanges() In Angular 2 Beta 3

By Ben Nadel on

In AngularJS 1.x, it was easy to tell when a $watch() callback was fired for the first time - the new value and the old value were the same (ie, there was no change to detect). With Angular 2 Beta 3, it's still just as easy to differentiate between the first run initialization phase of the ngOnChanges() directive life-cycle handler and the subsequent update phases; but, the mechanism has changed.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

With Angular 2, instead of being able to watch individual values with a $watch() binding, our directives can now register a single ngOnChanges() directive life-cycle handler that gets called when any of the directive inputs are set (either for the first time or for any subsequent updates). The change event object, passed to the ngOnChanges() handler, is a collection of the changed inputs in the current delta. It only includes the inputs that actually changed - inputs that remained the same are excluded from the collection.

Each value in the ngOnChanges event collection is an instance of the SimpleChange object has three keys:

  • currentValue
  • previousValue
  • isFirstChange()

It's the isFirstChange() method that allows us to determine if the given input is being initialized (ie, being set for the first time) or, if it's being updated some time after initialization.

To see this in action, I've created a simple component that takes a timestamp and a format and outputs a formatted time string. As the inputs to this component change, I am logging them to the console:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Differentiating Between Initialization And Update With ngOnChanges() In Angular 2 Beta 3
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • Differentiating Between Initialization And Update With ngOnChanges() In Angular 2 Beta 3
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ "AppComponent" ],
  • function run( AppComponent ) {
  •  
  • ng.platform.browser.bootstrap( AppComponent );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "AppComponent",
  • function registerAppComponent() {
  •  
  • var TimeLabel = require( "TimeLabel" );
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ TimeLabel ],
  • template:
  • `
  • Current Time:
  •  
  • <time-label
  • [value]="currentTime"
  • [format]="format">
  • </time-label>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I hold the current time-stamp inputs.
  • vm.currentTime = new Date();
  • vm.format = "simple";
  •  
  • // Start incrementing the time-stamp so that we can pass new values
  • // into the time-label component over time.
  • setInterval(
  • function incrementTime() {
  •  
  • vm.currentTime = new Date();
  •  
  • },
  • ( 1 * 1000 )
  • );
  •  
  • // After some time, change the formatting of the time.
  • setTimeout(
  • function changeFormat() {
  •  
  • vm.format = "full";
  •  
  • },
  • ( 3 * 1000 )
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a simple time-label component that formats the given time-stamp
  • // using the given algorithm.
  • define(
  • "TimeLabel",
  • function registerTimeLabel() {
  •  
  • // Configure the TimeLabel component definition.
  • ng.core
  • .Component({
  • selector: "time-label",
  • inputs: [ "value", "format" ],
  • template:
  • `
  • {{ timeAsString }}
  • `
  • })
  • .Class({
  • constructor: TimeLabelController,
  •  
  • // Define the component life-cycle events on the prototype so
  • // that they get picked up at run-time.
  • ngOnChanges: function noop() {}
  • })
  • ;
  •  
  • return( TimeLabelController );
  •  
  •  
  • // I control the TimeLabel component.
  • function TimeLabelController() {
  •  
  • var vm = this;
  •  
  • // I hold the formatted time-stamp value.
  • vm.timeAsString = "";
  •  
  • // Expose the public methods.
  • vm.ngOnChanges = ngOnChanges;
  •  
  •  
  • // ---
  • // PUBLIC MEHTODS.
  • // ---
  •  
  •  
  • // I handle a change in the component input values.
  • function ngOnChanges( event ) {
  •  
  • // The event object is a collection of "SimpleChange" change
  • // detection objects, each of which contains "currentValue",
  • // "previousValue", and a method - isFirstChange() - that
  • // determines whether or not this change represents the first
  • // change (ie, the initialization values) for the change detector.
  • if ( ( "value" in event ) && ( "format" in event ) ) {
  •  
  • console.log( "Initializing the component inputs." );
  • console.log( "Is first value?:", event.value.isFirstChange() );
  • console.log( "Is first format?:", event.format.isFirstChange() );
  •  
  • // Behind the scenes, each SimpleChange references the same
  • // "uninitialized" object reference. In fact, the way that
  • // state is determine is through strict-equality reference.
  • // As such, it just so happens that both of the previous
  • // values reference the same object.
  • // --
  • // CAUTION: I would never depend on this fact - I'm just
  • // including it here to get a sense of the mechanics.
  • console.log(
  • "Previous values match (strict equality):",
  • ( event.value.previousValue === event.format.previousValue )
  • );
  •  
  • // After the first, initializing change, only the updates values
  • // will exist in the change event object. In this demo, since
  • // "format" is not dynamic, it will never show up again in the
  • // ngOnChanges change collection.
  • } else {
  •  
  • console.log( "Updating the component inputs." );
  •  
  • // Check to see if value changed.
  • if ( "value" in event ) {
  •  
  • console.log( "Value changed for first time?:", event.value.isFirstChange() );
  •  
  • }
  •  
  • // Check to see if format changed.
  • if ( "format" in event ) {
  •  
  • console.log( "Format changed for first time?:", event.format.isFirstChange() );
  •  
  • }
  •  
  • }
  •  
  • vm.timeAsString = formatTime( vm.value, vm.format );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE MEHTODS.
  • // ---
  •  
  •  
  • // I format the given time-stamp using the given algorithm.
  • function formatTime( timestamp, format ) {
  •  
  • var raw = new Date( timestamp ).toTimeString();
  •  
  • if ( format === "simple" ) {
  •  
  • return( raw.split( " " ).shift() );
  •  
  • } else {
  •  
  • return( raw );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, both the timestamp and the format inputs will change over time, using setInterval() and setTimeout() respectively. When the ngOnChanges() life-cycle handler is invoked for the first time, all inputs will be present. After that, only the changed inputs will be present.

NOTE: All inputs can be present in the delta after the initial setup if they all happen change at the same time; but, in this controlled demo, I am using the dual existence in order to hook into the only time that I know both inputs are actually being set: during initialization.

With each change, I'm logging to see whether or not the input is being set for the first time; or, if it's being updated. And, when we run the above code, we get the following output:


 
 
 

 
 Differentiating between initialization and update in the ngOnChanges() directive life-cycle event handler. 
 
 
 

As you can see, all inputs were present on the ngOnChanges() event object during the initialization phase. Then, only the changed inputs were present on each subsequent update. And, .isFirstChange() only reported as true on the first run.

With Angular 2, change detection has moved from individual $watch() bindings to a collective ngOnChanges() directive life-cycle event handler. But, it looks like all the same machinery is there, allowing us to differentiate between values being set for the first time and values being updated after initialization.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.