Angular: Why you should consider using template outlet instead of content projection.

Content projection is a common approach to display children of component. The application I have been working on highly relied on it leading to performance issues accompanied with confusion. Template outlets solved this problem and I believe that you should consider using them instead.

Information in this article is based on Angular 2.x
Update: Angular 4 deprecates <template> in favor to <ng-template> and introduces new set of features which may be superior to method presented in this article.

What is content projection?

Content projection is a way to import HTML content from outside the component and insert that content into the component’s template in a designated spot. (link)

What’s the problem?

This example should help briefly explain the issue.

@Component({
selector: 'app',
template: `
<h1>Angular's content projection and lifecycle example</h1>
<app-content>
<app-nested-component></app-nested-component>
</app-content>
`,
})
export class App {}
@Component({
selector: 'app-content',
template: `
<button (click)="display = !display">Toggle content</button>
<ng-content *ngIf="display"></ng-content>
`,
})
export class Content {
display = false;
}
@Component({
selector: 'app-nested-component',
template: `
<b>Hello World!</b>
`,
})
export class NestedComponent implements OnDestroy, OnInit {

ngOnInit() {
alert('app-nested-component initialized!');
}

ngOnDestroy() {
alert('app-nested-component destroyed!');
}

}

It’s a specific case of using content projection. <app-content> has condition to whether display the content or not using ngIf directive on <ng-content>. <app-nested-component> is a child component. Pay attention to lifecycle hooks.

You check this example here.

What is happening?

Child component is immediately initialised, when not being in DOM, and it is not being destroyed when removed from DOM.

Why?

This is by design. The lifecycle of a component is always tied to the place where the component was declared, not to the place where the <ng-content> is used.

If you want to destroy the child component in this scenario, you have to destroy the parent component, which is not a flexible solution.

Why you should care about it?

Destroying, creating or even changing the component with content projection can cause tremendous performance issues!

Imagine the scenario where your application has component containing X tabs. Every tab has content as component with more complex functionality than displaying text. All X components will be initialised and execute logic existing in ngOnInit. Every app re-render will cause all X components to call update related events. Did you notice the problem?
Instead of calling lifecycle events for one displayed component, we’re calling lifecycle events of X components causing too big overhead when application gets bigger.

Using non-angular libraries querying DOM elements is very problematic with content projection due to its nature.

But don’t worry, there is hope.

Template outlet to the rescue!

Template outlet is still an experimental feature, but also a perfect remedy for presented problem. ngTemplateOutlet inserts an embedded view from a prepared TemplateRef. (link)

Take a look at example below to make it clear.

<template [ngTemplateOutlet]="exampleTemplateRef"></template>
<template #exampleTemplateRef>
<b>Example template<b>
</template>

We are binding template’s reference to ngTemplateOutlet, so its content will be displayed.

Using template outlet instead of content projection.

We will have to approach our problem a little bit different due to how template outlet works. Take a look at solution below. Changes are highlighted.

@Component({
selector: 'app',
template: `
<h1>Angular's template outlet and lifecycle example</h1>
<app-content [templateRef]="nestedComponentRef"></app-content>
<template #nestedComponentRef>
<app-nested-component></app-nested-component>
</template>

`,
})
export class App {}
@Component({
selector: 'app-content',
template: `
<button (click)="display = !display">Toggle content</button>
<template
*ngIf="display"
[ngTemplateOutlet]="templateRef">
</template>

`,
})
export class Content {
display = false;
@Input() templateRef: TemplateRef;
}
@Component({
selector: 'app-nested-component',
template: `
<b>Hello World!</b>
`,
})
export class NestedComponent implements OnDestroy, OnInit {

ngOnInit() {
alert('app-nested-component initialized!');
}

ngOnDestroy() {
alert('app-nested-component destroyed!');
}

}

We are binding template’s reference to <app-content> which binds it to ngTemplateOutlet. Thanks to it <app-nested-component> is being destroyed and initialised as expected.

You can check this example here.

In summary

If you want to use content projection to conditionally display more complex components, consider using template outlet to make Angular call proper lifecycle events when component is being displayed or not to avoid performance issues.

Thanks for reading!

Any feedback is appreciated.

Useful links: