Correlating Directive Life-Cycle Events To DOM State In AngularJS 2 Beta 1
Most of the time, in AngularJS 1.x, you didn't need to know about the state of the DOM (Document Object Model); you just updated the view-model and the DOM would eventually be reconciled. Every now and then, however, hooking into that rendering life-cycle would have been super helpful. With AngularJS 2, we finally get those life-cycle hooks. But, to be honest, I found the documentation on the life-cycle hooks to be confusing since it included a lot of new terminology. As such, I wanted to put together a small demo that explores the correlation of the life-cycle events to the state of the DOM rendering.
Run this demo in my JavaScript Demos project on GitHub.
When it comes to the AngularJS 2 life-cycle events, the terminology that I found confusing was:
- "Content" - which part of the view is that?
- "View" - how is that different from the content?
- "Checked" - what the heck does it mean for a view to be checked?
To find some clarity on this, I put together a small demo that increments a counter. It then uses this counter to define both an input to a component as well as the transcludable content of the component. The test component then uses that same counter to render its own view, which includes a child component that also accepts the counter. In this way, we can see how the value of the counter is propagated down through the DOM tree during various life-cycle events.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Correlating Directive Life-Cycle Events To DOM State In AngularJS 2 Beta 1
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Correlating Directive Life-Cycle Events To DOM State In AngularJS 2 Beta 1
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-all.umd.min.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/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 control the root of the application.
define(
"AppComponent",
[ "LifeCycleTest" ],
function registerAppComponent( LifeCycleTest ) {
// Define the AppComponent component metadata.
var AppComponent = ng.core
.Component({
selector: "my-app",
directives: [ LifeCycleTest ],
// Note that we are both defining the COUNTER both as an input
// property for the test component and as a dynamic portion of
// the transcludable content.
template:
`
<p>
<a (click)="incrementCounter()">Increment Counter</a>
—
<a (click)="0">No-op</a>
</p>
<life-cycle-test [counter]="counter">
( {{ counter }} )
</life-cycle-test>
`
})
.Class({
constructor: AppController
})
;
return( AppComponent );
// I control the App component.
function AppController() {
var vm = this;
// I provide a way to keep the content dynamic.
vm.counter = 1;
// Expose the public methods.
vm.incrementCounter = incrementCounter;
// ---
// PUBLIC METHODS.
// ---
// I increment the counter, kicking off a new round of change-detection.
function incrementCounter() {
console.log( "- - - - - - - - - - - - - - - - " );
vm.counter++;
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a test component that logs the HTML of the component during
// various AngularJS component life-cycle events. The component view is composed
// of internal content, transcluded content, and nested content.
define(
"LifeCycleTest",
[ "ChildTest" ],
function registerLifeCycleTest( ChildTest ) {
// Define the LifeCycleTest component metadata.
var LifeCycleTestComponent = ng.core
.Component({
selector: "life-cycle-test",
inputs: [ "counter" ],
directives: [ ChildTest ],
// Here, we are using the counter in a number of ways. Not only
// are we using simple interpolation and transclusion, we're also
// generating dynamic markup based on the counter.
template:
`
<span *ngIf="( ( counter % 2 ) == 0 )">Even:</span>
<span *ngIf="( ( counter % 2 ) == 1 )">Odd:</span>
View: {{ counter }},
Content: <ng-content></ng-content>,
<child-test [counter]="counter">
( {{ counter }} )
</child-test>
`
})
.Class({
constructor: LifeCycleTestController,
// Define life-cycle methods on the prototype so that they'll
// be picked up during runtime execution.
ngOnChanges: function noop() {},
ngOnInit: function noop() {},
// ngDoCheck: function noop() {},
ngAfterContentInit: function noop() {},
ngAfterContentChecked: function noop() {},
ngAfterViewInit: function noop() {},
ngAfterViewChecked: function noop() {}
})
;
// Configure the dependency-injection for the controller constructor.
// We'll need a reference to the host element in order to query its
// rendered content.
LifeCycleTestComponent.parameters = [ new ng.core.Inject( ng.core.ElementRef ) ];
return( LifeCycleTestComponent );
// I control the LifeCycleTest component.
function LifeCycleTestController( element ) {
var vm = this;
// Expose the public methods.
vm.ngOnChanges = ngOnChanges;
vm.ngOnInit = ngOnInit;
vm.ngAfterContentInit = ngAfterContentInit;
vm.ngAfterContentChecked = ngAfterContentChecked;
vm.ngAfterViewInit = ngAfterViewInit;
vm.ngAfterViewChecked = ngAfterViewChecked;
// ---
// PUBLIC METHODS.
// ---
// I get called whenever the input values have changed, before any
// of the views are updated.
function ngOnChanges( event ) {
// NOTE: Outputting state of the counter as well as the content.
console.log( "ngOnChanges[", this.counter, "] ...", content() );
}
// I get called once after the component has been instantiated
// and the inputs have been bound, but before the views have been
// rendered.
function ngOnInit() {
console.log( "ngOnInit ...", content() );
}
// I get called after the directive's content (ie, the transcludable
// content) has been initialized.
function ngAfterContentInit() {
console.log( "ngAfterContentInit ...", content() );
}
// I get called after the directive's content (ie, the transcludable
// content) has been updated.
function ngAfterContentChecked() {
console.log( "ngAfterContentChecked ...", content() );
}
// I get called once, after ngAfterContentInit() is called, after
// the COMPONENT's view has been initialized.
function ngAfterViewInit() {
console.log( "ngAfterViewInit ...", content() );
}
// I get called after the COMPONENT's view has been updated.
function ngAfterViewChecked() {
console.log( "ngAfterViewChecked ...", content() );
}
// ---
// PRIVATE METHODS.
// ---
// I return the content text of the host element.
function content() {
var text = element.nativeElement.textContent
.replace( /\s+/g, " " )
.trim()
;
return( text );
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a simple, nested Child component to also renders the counter as
// both an input and as transcluded content.
define(
"ChildTest",
function registerChildTestTest() {
// Define the ChildTest component metadata.
var ChildTestComponent = ng.core
.Component({
selector: "child-test",
inputs: [ "counter" ],
// Notice that we are using both the incoming property and the
// transcluded content to generate our component view.
template:
`
Child-View: {{ counter }},
Child-Content: <ng-content></ng-content>
`
})
.Class({
constructor: function noop() {}
})
;
return( ChildTestComponent );
}
);
</script>
</body>
</html>
It might be easier to follow this if you watch the video. But, when we run this page and increment the counter a few times, we get the following output:
Based on this output, I think we can come to a few conclusions about the confusing terminology from above:
- Content - this is the child content of the host element. For a component directive, this is the transcludable content; and, for an attribute directive, this is simply the inner content.
- View - this is the template provided by the component directive (and does not apply to attribute directives). In the case of transclusion, this is the DOM tree into which the external content is being transcluded.
- Checked - this appears to mean that the given DOM has been reconciled / synchronized with the recent view-model changes.
In AngularJS 1.x, there was always a sense that you couldn't quite depend on the state of the DOM. As such, there was a lot of $timeout() or $evalAsync() usage in order to ensure that the DOM had been given time to synchronize. In AngularJS 2, it's going to be really nice to be able to hook into that life-cycle with confidence.
Want to use code from this post? Check out the license.
Reader Comments