Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Critter Gewlas
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Critter Gewlas@Critter )

Ng-Content Life-Cycle Is Controlled By The Parent View Not The Consumer In Angular 2 Beta 11

By Ben Nadel on

I know that the title is kind of a mouth-full. And, I'm not sure that the terminology is exactly correct. But, I wanted to put together a quick little demo to showcase a property of Angular 2's projected content that was not immediately obvious to me. When an Angular 2 component projects content into its own view using ng-content, the life-cycle of the projected content is not controlled by the consumer / projector - it's controlled by the parent view in which the projected content was originally provided.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

I stumbled upon this behavior when I was building my HTML Dropdown menu component in Angular 2. In that demo, the dropdown menu items were provided by the calling context, not by the dropdown itself. This means (as I'll demonstrate) that the menu items existed regardless of whether or not the dropdown menu component was currently projecting them.

To see this more clearly, I've put together a simple demo in which we have a container component that projects a sub-component into its own view. Both the container and the sub-component can be toggled at the top level. And, we'll be logging-out the relevant life-cycle events in both components to see when the sub-component is actually created and destroyed.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Ng-Content Life-Cycle Is Controlled By The Parent View Not The Consumer In Angular 2 Beta 11
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • Ng-Content Life-Cycle Is Controlled By The Parent View Not The Consumer In Angular 2 Beta 11
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • ng.platform.browser.bootstrap( require( "App" ) );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • directives: [
  • require( "ContentConsumer" ),
  • require( "ContentItem" )
  • ],
  •  
  • // In this demo, we have two components - the Content Consumer
  • // and the Content Item. We're going to be projecting the Content
  • // Item into the Content Consumer (via ng-content) in order to
  • // demonstrate which context (the parent or the consumer) can
  • // affect the ng-content life-cycle events.
  • template:
  • `
  • <p>
  • <a (click)="toggleContainer()">Toggle Countainer</a>
  • &mdash;
  • <a (click)="toggleContent()">Toggle Content</a>
  • </p>
  •  
  • <content-consumer
  • *ngIf="isShowingContainer"
  • [isShowingContent]="isShowingContent">
  •  
  • <content-item>
  • ... Now, plain zero!
  • </content-item>
  •  
  • </content-consumer>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I determine which parts of the view are being rendered. While
  • // this component directly controls the visibility of the consumer,
  • // we are deferring to the consumer to show the projected content.
  • vm.isShowingContainer = false;
  • vm.isShowingContent = false;
  •  
  • // Expose the public methods.
  • vm.toggleContainer = toggleContainer;
  • vm.toggleContent = toggleContent;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I toggle the existence of the content consumer.
  • function toggleContainer() {
  •  
  • vm.isShowingContainer = ! vm.isShowingContainer;
  •  
  • }
  •  
  •  
  • // I toggle the existence of the content item.
  • // --
  • // NOTE: This property is being passed into the consumer which will
  • // be using the [ngIf] to toggle the ng-content internally.
  • function toggleContent() {
  •  
  • vm.isShowingContent = ! vm.isShowingContent;
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the ng-content consumer.
  • define(
  • "ContentConsumer",
  • function registerContentConsumer() {
  •  
  • // Configure the ContentConsumer component definition.
  • ng.core
  • .Component({
  • selector: "content-consumer",
  • inputs: [ "isShowingContent" ],
  •  
  • // Notice that we are toggling the projection of the ng-content
  • // block based on the [isShowingContent] input property.
  • template:
  • `
  • Here is Sub-Zero:
  •  
  • <div *ngIf="isShowingContent">
  • <ng-content></ng-content>
  • </div>
  • `
  • })
  • .Class({
  • constructor: ContentConsumerController,
  •  
  • // Define the life-cycle event methods on the prototype so that
  • // they are picked up at run-time.
  • ngOnChanges: function noop() {}
  • })
  • ;
  •  
  • return( ContentConsumerController );
  •  
  •  
  • // I control the ContentConsumer component.
  • function ContentConsumerController() {
  •  
  • var vm = this;
  •  
  • // Expose the public methods.
  • vm.ngOnChanges = ngOnChanges;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I get called any time at least one of the bound inputs is changed.
  • function ngOnChanges( changes ) {
  •  
  • console.log( "[isShowingContent]:", vm.isShowingContent );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the ng-content item that logs its own life-cycle events.
  • define(
  • "ContentItem",
  • function registerContentItem() {
  •  
  • // Configure the ContentItem component definition.
  • ng.core
  • .Component({
  • selector: "content-item",
  • template:
  • `
  • <ng-content></ng-content>
  • `
  • })
  • .Class({
  • constructor: ContentItemController,
  •  
  • // Define the life-cycle event methods on the prototype so that
  • // they are picked up at run-time.
  • ngOnDestroy: function noop() {},
  • ngOnInit: function noop() {}
  • })
  • ;
  •  
  • return( ContentItemController );
  •  
  •  
  • // I control the ContentItem component.
  • function ContentItemController() {
  •  
  • var vm = this;
  •  
  • // Expose the public methods.
  • vm.ngOnDestroy = ngOnDestroy;
  • vm.ngOnInit = ngOnInit;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I get called once when the component is destroyed.
  • function ngOnDestroy() {
  •  
  • console.warn( "Content Item destroyed!" );
  •  
  • }
  •  
  •  
  • // I get called once when the component is initialized.
  • function ngOnInit() {
  •  
  • console.warn( "Content Item initialized!" );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we're hooking into the ContentItem's ngOnInit() and ngOnDestroy() life-cycle events so that we can see who actually controls the life-cycle for the projected content. Then, we're hooking into the ContentConsumer's ngOnChanges() life-cycle event so that we can see when the projected content is actually being projected. And, when we run this code and toggle the the container and then the content, we get the following output:


 
 
 

 
 ng-content life-cycle is controlled by the parent view, not the consuming component. 
 
 
 

As you can see, even as we toggle the projection of the ng-content on and off, the projected component - ContentItem - continues to exist. The ContentConsumer has control over the projection of the content, but not over its existence. The ContentItem isn't actually destroyed until its parent view destroys the ContentConsumer component tree itself.

The only reason that I wanted to showcase this behavior is because it's slightly different than the behavior in AngularJS 1.x. In AngularJS 1.x, transcluded content goes through a very explicit cloning and linking phase when its being introduced to the rendered DOM (Document Object Model). In Angular 2, this change in life-cycle behavior may not be obvious.

NOTE: Angular 2 can still dynamically instantiate and inject components and templates; but, that's not exactly the workflow that I'm concerned with in this post.




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.