Introduction
The single responsibility principle is the idea that pieces of your application should have one purpose. Following this principle makes your Angular app easier to test and develop.
In Angular, using NgTemplateOutlet
instead of creating specific components allows for components to be easily modified for various use cases without having to modify the component itself!
In this article, you will take an existing component and rewrite it to use NgTemplateOutlet
.
Prerequisites
To complete this tutorial, you will need:
Node.js installed locally, which you can do by following How to Install Node.js and Create a Local Development Environment.
Some familiarity with setting up an Angular project.
This tutorial was verified with Node v16.6.2, npm
v7.20.6, and @angular/core
v12.2.0.
Step 1 – Constructing CardOrListViewComponent
Consider CardOrListViewComponent
which displays items
in a 'card'
or a 'list'
format depending on its mode
.
It consists of a card-or-list-view.component.ts
file:
card-or-list-view.component.ts
import {
Component,
Input
} from '@angular/core';
@Component({
selector: 'card-or-list-view',
templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {
@Input() items: {
header: string,
content: string
}[] = [];
@Input() mode: string = 'card';
}
And a card-or-list-view.component.html
template:
card-or-list-view.component.html
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="'card'">
<div *ngFor="let item of items">
<h1>{{item.header}}</h1>
<p>{{item.content}}</p>
</div>
</ng-container>
<ul *ngSwitchCase="'list'">
<li *ngFor="let item of items">
{{item.header}}: {{item.content}}
</li>
</ul>
</ng-container>
Here is an example of the usage of this component:
usage.component.ts
import { Component } from '@angular/core';
@Component({
template: `
<card-or-list-view
[items]="items"
[mode]="mode">
</card-or-list-view>
`
})
export class UsageExample {
mode = 'list';
items = [
{
header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
content: 'The single responsibility principle...'
} // ... more items
];
}
This component does not have a single responsibility and isn’t very flexible. It needs to keep track of its mode
and know how to display items
in both card
and list
view. And it can only display items
with a header
and content
.
Let’s change that by breaking the component into separate views using templates.
Step 2 – Understanding ng-template
and NgTemplateOutlet
In order to allow the CardOrListViewComponent
to display any kind of items
we need to be able to tell it how to display them. We can achieve this by giving it a template that it can use to stamp out the items
.
The templates will be TemplateRefs
using <ng-template>
and the stamps will be EmbeddedViewRefs
created from the TemplateRefs
. EmbeddedViewRefs
represent views in Angular with their own context and are the smallest essential building block.
Angular provides a way to use this concept of stamping out views from templates with NgTemplateOutlet
.
NgTemplateOutlet
is a directive that takes a TemplateRef
and context and stamps out an EmbeddedViewRef
with the provided context. The context is accessed on the template via let-{{templateVariableName}}="contextProperty"
attributes to create a variable the template can use. If a context property name is not provided, it will choose the $implicit
property.
Here is an example:
import { Component } from '@angular/core';
@Component({
template: `
<ng-container *ngTemplateOutlet="templateRef; context: exampleContext"></ng-container>
<ng-template #templateRef let-default let-other="aContextProperty">
<div>
$implicit = '{{default}}'
aContextProperty = '{{other}}'
</div>
</ng-template>
`
})
export class NgTemplateOutletExample {
exampleContext = {
$implicit: 'default context property when none specified',
aContextProperty: 'a context property'
};
}
Here is the output from the example:
<div>
$implicit = 'default context property when none specified'
aContextProperty = 'a context property'
</div>
The default
and other
variables are provided by the let-default
and let-other="aContextProperty"
props.
Step 3 – Refactoring CardOrListViewComponent
To provide flexibility to the CardOrListViewComponent
and allow it to display any type of items
, we will create two structural directives to read in as templates. These templates will be the card and list item.
Here is card-item.directive.ts
:
card-item.directive.ts
import { Directive } from '@angular/core';
@Directive({
selector: '[cardItem]'
})
export class CardItemDirective {
constructor() { }
}
And here is list-item.directive.ts
:
list-item.directive.ts
import { Directive } from '@angular/core';
@Directive({
selector: '[listItem]'
})
export class ListItemDirective {
constructor() { }
}
CardOrListViewComponent
will import CardItemDirective
and ListItemDirective
:
card-or-list-view.component.ts
import {
Component,
ContentChild,
Input,
TemplateRef
} from '@angular/core';
import { CardItemDirective } from './card-item.directive';
import { ListItemDirective } from './list-item.directive';
@Component({
selector: 'card-or-list-view',
templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {
@Input() items: {
header: string,
content: string
}[] = [];
@Input() mode: string = 'card';
@ContentChild(CardItemDirective, {read: TemplateRef}) cardItemTemplate: any;
@ContentChild(ListItemDirective, {read: TemplateRef}) listItemTemplate: any;
}
This code will read in our structural directives as TemplateRefs
.
card-or-list-view.component.html
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="'card'">
<ng-container *ngFor="let item of items">
<ng-container *ngTemplateOutlet="cardItemTemplate"></ng-container>
</ng-container>
</ng-container>
<ul *ngSwitchCase="'list'">
<li *ngFor="let item of items">
<ng-container *ngTemplateOutlet="listItemTemplate"></ng-container>
</li>
</ul>
</ng-container>
Here is an example of the usage of this component:
usage.component.ts
import { Component } from '@angular/core';
@Component({
template: `
<card-or-list-view
[items]="items"
[mode]="mode">
<div *cardItem>
Static Card Template
</div>
<li *listItem>
Static List Template
</li>
</card-or-list-view>
`
})
export class UsageExample {
mode = 'list';
items = [
{
header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
content: 'The single responsibility principle...'
} // ... more items
];
}
With these changes, the CardOrListViewComponent
can now display any type of item in the card or list form based on the template provided. Currently, the templates are static.
The last thing we need to do is allow the templates to be dynamic by giving them a context:
card-or-list-view.component.html
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="'card'">
<ng-container *ngFor="let item of items">
<ng-container *ngTemplateOutlet="cardItemTemplate; context: {$implicit: item}"></ng-container>
</ng-container>
</ng-container>
<ul *ngSwitchCase="'list'">
<li *ngFor="let item of items">
<ng-container *ngTemplateOutlet="listItemTemplate; context: {$implicit: item}"></ng-container>
</li>
</ul>
</ng-container>
Here is an example of the usage of this component:
usage.component.ts
import { Component } from '@angular/core';
@Component({
template: `
<card-or-list-view
[items]="items"
[mode]="mode">
<div *cardItem="let item">
<h1>{{item.header}}</h1>
<p>{{item.content}}</p>
</div>
<li *listItem="let item">
{{item.header}}: {{item.content}}
</li>
</card-or-list-view>
`
})
export class UsageExample {
mode = 'list';
items = [
{
header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
content: 'The single responsibility principle...'
} // ... more items
];
}
The interesting thing to note is that we use the asterisk prefix and microsyntax for syntactical sugar. It is the same as:
<ng-template cardItem let-item>
<div>
<h1>{{item.header}}</h1>
<p>{{item.content}}</p>
</div>
</ng-template>
And that’s it! We have the original functionality, but now we can display whatever we want by modifying the templates and the CardOrListViewComponent
has less responsibility. We can add more to the item context like first
or last
similar to ngFor
or display completely different types of items
.
Conclusion
In this article, you took an existing component and rewrote it to use NgTemplateOutlet
.
If you’d like to learn more about Angular, check out our Angular topic page for exercises and programming projects.