Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Adam Presley
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Adam Presley@adampresley )

Component Life-Cycle Methods Need To Be Defined On The Prototype In AngularJS 2 Beta 1

By Ben Nadel on

With AngularJS 1.x, we had constructors, the $watch() method, and the $destroy event as a way to try and hook into various points in a Controller's life-cycle. With AngularJS 2 Beta 1, we now have official, granular life-cycle events that we can bind to by way of the controller's public methods. These include, but are not limited to, ngOnInit (think constructor-ish), ngOnDestroy (think $destroy event), and ngOnChanges (think $watch handler). As I've been experimenting with AngularJS 2, however, one thing that tripped me up is the fact that these event handlers have to be defined on the controller's prototype otherwise Angular won't invoke them at the appropriate times (meaning, not at all).


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Now, when I say that these event-handler methods need to be defined on the Controller's prototype, I don't necessarily mean that they have to be "implemented" on the prototype; they just have to exist there. While I still don't really know how to explore the AngularJS 2 source code yet, it appears that Angular is inspecting the prototype of the class definition when trying to determine if it should invoke the Controller's life-cycle methods. However, when it goes to invoke said methods, it does so on the Controller instance (which, of course, inherits from its own prototype).

To demonstrate this, I've put together a small demo in which three different child components are toggled into and out of existence (ie, not just shown and hidden). Each of the three child components uses a different event-handler binding strategy:

  1. Defined and implemented on the constructor prototype.
  2. Defined and implemented on the instance.
  3. Defined on the constructor prototype but implemented on the instance.

In the following code, I am explicitly defining the constructor prototype instead of using the .Class() method so as to make things a bit more explicit:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Component Life-Cycle Methods Need To Be Defined On The Prototype In AngularJS 2 Beta 1
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Component Life-Cycle Methods Need To Be Defined On The Prototype 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>
  • <!--
  • Putting Almond.js after the external libraries since I am not managing their
  • loading through Almond. If I move this library up, AngularJS attempts to use
  • define() (I think) which I am not configured for in this demo.
  • --
  • NOTE: AlmondJS is just a simplified version of RequireJS.
  • -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // In order to have all my code in one page and to not have to care about the
  • // order in which things are defined, I am using AlmondJS (ie, RequireJS) to
  • // manage the registration of "class files" (so to speak). This is more aligned
  • // with how a class loader would work.
  • requirejs(
  • [ "AppComponent" ],
  • function( AppComponent ) {
  •  
  • ng.platform.browser.bootstrap( AppComponent );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • define(
  • "AppComponent",
  • [ "LCProto", "LCInstance", "LCBoth" ],
  • function( LCProto, LCInstance, LCBoth ) {
  •  
  • // Configure the App component definition.
  • var AppComponent = ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ LCProto, LCInstance, LCBoth ],
  • template:
  • `
  • <p>
  • <a (click)="toggleChildren()">Toggle Child Components</a>
  • </p>
  •  
  • <div *ngIf="isShowingChildren">
  •  
  • <life-cycle-proto></life-cycle-proto>
  • <life-cycle-instance></life-cycle-instance>
  • <life-cycle-both></life-cycle-both>
  •  
  • </div>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppComponent );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I determine if the child components are showing. Since we are
  • // using ngIf, we are actually toggling the existence of said
  • // components on the DOM (ie, not just hiding them).
  • vm.isShowingChildren = false;
  •  
  • // Expose the public methods.
  • vm.toggleChildren = toggleChildren;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I toggle the existence of the child components.
  • function toggleChildren() {
  •  
  • console.log( "- - - - - - - - - - - - - -" );
  •  
  • vm.isShowingChildren = ! vm.isShowingChildren
  •  
  • };
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a component in which the life-cycle methods are defined on the
  • // prototype but NOT on the instance.
  • define(
  • "LCProto",
  • function() {
  •  
  • // Configure the life-cycle component definition.
  • var LCProtoComponent = ng.core
  • .Component({
  • selector: "life-cycle-proto",
  • template:
  • `
  • <p>
  • Life-Cycle methods defined on <strong>constructor prototype</strong>.
  • </p>
  • `
  • })
  • .Class({
  • constructor: LCProtoController
  • })
  • ;
  •  
  • // NOTE: Component is being returned down below in order to allow the
  • // prototype to be fully defined. I could have used .Class() function
  • // above, but would have bade for odd readability.
  • // --
  • // return( LCProtoComponent );
  •  
  •  
  • // I control the life-cycle demo component.
  • function LCProtoController() {
  •  
  • this.bindType = "Defined on constructor prototype.";
  •  
  • }
  •  
  • // Define life-cycle methods on the constructor prototype.
  • LCProtoController.prototype = {
  •  
  • ngOnInit: function() {
  •  
  • console.log( "ngOnInit:", this.bindType );
  •  
  • },
  •  
  • ngOnDestroy: function() {
  •  
  • console.log( "ngOnDestroy", this.bindType )
  •  
  • }
  •  
  • };
  •  
  • // CAUTION: We have to return out of the enclosure AFTER the prototype
  • // has been defined or it will never get wired to the constructor
  • // (since only the constructor is hoisted).
  • return( LCProtoComponent );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a component in which the life-cycle methods are defined on the
  • // instance but NOT on the prototype.
  • define(
  • "LCInstance",
  • function() {
  •  
  • // Configure the life-cycle component definition.
  • var LCInstanceComponent = ng.core
  • .Component({
  • selector: "life-cycle-instance",
  • template:
  • `
  • <p>
  • Life-Cycle methods defined on <strong>instance</strong>.
  • </p>
  • `
  • })
  • .Class({
  • constructor: LCInstanceController
  • })
  • ;
  •  
  • return( LCInstanceComponent );
  •  
  •  
  • // I control the life-cycle demo component.
  • function LCInstanceController() {
  •  
  • var bindType = "Defined on instance.";
  •  
  •  
  • // Define life-cycle method on instance.
  • this.ngOnInit = function() {
  •  
  • console.log( "ngOnInit:", bindType );
  •  
  • };
  •  
  • // Define life-cycle method on instance.
  • this.ngOnDestroy = function() {
  •  
  • console.log( "ngOnDestroy:", bindType );
  •  
  • };
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a component in which the life-cycle methods are defined on the
  • // prototype (as an interface) and then on the instance (as an implementation).
  • define(
  • "LCBoth",
  • function() {
  •  
  • // Configure the life-cycle component definition.
  • var LCBothComponent = ng.core
  • .Component({
  • selector: "life-cycle-both",
  • template:
  • `
  • <p>
  • Life-Cycle methods defined on both <strong>prototype and instance</strong>.
  • </p>
  • `
  • })
  • .Class({
  • constructor: LCBothController,
  •  
  • // It doesn't matter what these references are, as long as they
  • // are Functions. The actual implementation is handled by the
  • // instance at runtime; but, the instance methods won't be called
  • // unless there is an existing reference on the prototype.
  • // --
  • // NOTE: "noop" stand for "no-op" or "No operation."
  • ngOnInit: function noop() {},
  • ngOnDestroy: function noop() {}
  • })
  • ;
  •  
  • return( LCBothComponent );
  •  
  •  
  • // I control the life-cycle demo component.
  • function LCBothController() {
  •  
  • var bindType = "Defined on both prototype and instance.";
  •  
  •  
  • // Define life-cycle method on instance.
  • this.ngOnInit = function() {
  •  
  • console.log( "ngOnInit:", bindType );
  •  
  • };
  •  
  • // Define life-cycle method on instance.
  • this.ngOnDestroy = function() {
  •  
  • console.log( "ngOnDestroy:", bindType );
  •  
  • };
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, each life-cycle component implements the ngOnInit and ngOnDestroy event handlers. However, when we run the following code, you will see that not all component bindings act as expected:


 
 
 

 
 Life-cycle methods, in AngularJS 2, are only invoked when defined on the prototype. 
 
 
 

As you can see, we have three components, but only two sets of working event bindings. Both components that had something defined on the prototype worked, even when the actual implementation was on the controller instance. The only one that failed to execute was the component in which the event handlers were both defined and implemented on the Controller instance.

If it's unclear why this is happening, take a look at the difference between prototypal inspection vs. method invocation:


 
 
 

 
 Life-cycle methods can be defined on the prototype and then inherited (or overridden) by the instance. 
 
 
 

At invocation time, all of the event-handlers are bound to the instance, even if they were fully defined and implemented on the prototype. But, if they were only defined on the instance, Angular never bothers invoking the event handlers.

I am sure that there are many people who wonder why I would ever want to try to write AngularJS 2 using ES5. Well, this is exactly why; it's already teaching me stuff about how AngularJS 2 is wired together - finer points that I may have never understood had I just used ES6 classes from the start. I truly believe that building out this mental model will help me understand and debug AngularJS 2 code in the future.




Reader Comments

I think you might be thinking too much in an NG1 "controller" way.
This works nicely, I think:

var LCInstanceComponent = ng.core
.Component({
selector: "life-cycle-instance",
template: `...`
})
.Class({
constructor: function() {
this.bindType = "Defined on instance.";
},
ngOnInit: function() {
console.log( "ngOnInit:", this.bindType );
},
ngOnDestroy: function() {
console.log( "ngOnDestroy:", this.bindType );
}
});

Reply to this Comment

@Fergus,

We are doing the same thing, I think. I'm just being more explicit about where the prototype is for the sake of explanation. When you pass additional methods to the .Class() function, you are defining them on the prototype. So:

.Class({ foo: function(){ ... } })

... is really the same thing as:

YourClass.prototype = { foo: function() { ... } };

... at least I *think* that is what is going on. I'm still not good at finding my way around the Angular 2 source code yet. And, even with the NG1 source code, the compile code was still somewhat of a mystery :D

Reply to this Comment

I think the problem with your original LCInstanceController is that "this" refer LCInstanceController but NG is looking for ngOnInit() on the LCInstanceComponent "class". The scoping is out of whack.

Reply to this Comment

@Fergus,

I would agree if only the .Class() example worked. But, both the .Class() example and the LCProto component example work. As such, I think it's just that it examines the prototype, regardless of whether or not I am using .Class() to define the methods.

Reply to this Comment

Hey Ben,

again great post!

Here is explanation why it checks for life cycle methods on prototype.

Whole( I mean a lot of ) angular 2 relies on decorators. In class decorators you don't get access to instance types, only yourClassConstructorFunction.prototype are available, so that's the main reasoning behind this.

also check the implementation for life cycle hooks reflector. Prototype everywhere.
https://github.com/angular/angular/blob/master/modules/angular2/src/core/linker/directive_lifecycle_reflector.ts

and at the end of the day. I think that's better choice, than having those hooks directly on instance and wasting memory :)

Reply to this Comment

@Martin,

Thank you good sir. I'm still having a lot of trouble finding my way around the source code. In AngularJS 1.x, it was really easy - it was all just right there, really easy to read. I've been trying to use the *bundle* files as a way to quickly look things up; but, they aren't really meant to be read - just concatenated :D

I am sure that I will eventually move to more ES6 and the use of prototypes. But, for right now, I am finding the ES5 approach as a really good way to peel back the curtain and see what they heck is going on.

Reply to this Comment

@Ben,

hehe yeah, well bundle is just readable like, mmm, well ... bundle :D

but I completely understand your reasoning to stick with es5 for now.
But trust me, once you start with ES6, you'll never look back ;)

Reply to this Comment

I think Martin is right. I respectfully suggest you should stop thinking that Angular2 is Angular1 v2 and embrace ES6 (or, even better, Typescript)

Reply to this Comment

@Martin, @Fergus,

I do definitely try to map my understanding of NG1 onto NG2 ... but I don't think that's wrong, per say. It just needs to be translated as I go.

Reply to this Comment

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.