What is ngTemplateOutlet in Angular? (And when to use it)
ngTemplateOutlet is more useful than you might realize
What is ngTemplateOutlet
in Angular?
Many just don't get the use case for ngTemplateOutlet
. And can't seem to understand why use it in your own code.
And it gets worse...
If you got to the official documentation for ngTemplateOutlet you'll see a description like this.
Inserts an embedded view from a prepared TemplateRef.
You can attach a context object to the EmbeddedViewRef by setting [ngTemplateOutletContext]. [ngTemplateOutletContext] should be an object, the object's keys will be available for binding by the local template let declarations.
That just makes it more confusing huh?
So today we're going to build a customizable table component and this will help you see the reason for ngTemplateOutlet
.
What is ngTemplate and ngTemplateOutlet?
So the main idea is that we can use ngTemplate
to create a chunk of a template that won't actually be rendered to the DOM.
And instead we can dynamically control when to display this template. This was very common before the new Angular control flow.
A common example of this would be a loading template while waiting for something else to finish.
<p *ngIf="false; else myTemplate">Done</p>
<ng-template #myTemplate>
Done
</ng-template>
And then we could use this template over and over again with the help of ngTemplateOutlet
.
<p *ngIf="false; else myTemplate">Done</p>
<ng-template #myTemplate>
Done
</ng-template>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
<ng-container *ngTemplateOutlet="myTemplate"></ng-container>
And we could also configure the Angular template to accept variables. 😎 😎 😎
<ng-template #myTemplate let-greeting="greeting" let-message="message">
<p>{{greeting}} {{message}}</p>
</ng-template>
<ng-container *ngTemplateOutlet="myTemplate; context: {greeting: 'Hi', message: 'Angular'}"></ng-container>
You're probably noticing that this is kind of like what a component does. You can create a child Angular component by passing in different inputs to control what it displays.
So why not just use a component instead of defining all these weird template outlets?
Well, templates can give us more power, if we use them well.
Follow along buddy!
To demonstrate the power of ngTemplateOutlet
we're going to create a dynamic table component.
1. Creating the template
We can use the Angular CLI to create our dynamic table.
ng generate component table
And then our demo code.
import { CommonModule } from '@angular/common';
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
@Component({
selector: 'app-table',
standalone: true,
imports: [CommonModule],
template: `
<table>
<thead>
<tr>
<ng-container
*ngTemplateOutlet="
headers || defaultHeaderTemplate;
context: { $implicit: data }
"
></ng-container>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data">
<ng-container
*ngTemplateOutlet="
rows || defaultRowTemplate;
context: { $implicit: row }
"
></ng-container>
</tr>
</tbody>
</table>
<ng-template #defaultHeaderTemplate let-data>
<th *ngFor="let header of data[0] | keyvalue">{{ header.key }}</th>
</ng-template>
<ng-template #defaultRowTemplate let-row>
<td *ngFor="let row of row | keyvalue">{{ row.value }}</td>
</ng-template>
`,
styles: [
`
::ng-deep table {
width: 100%;
margin: 2rem 0;
border-collapse: collapse;
font-family: sans-serif;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
thead {
tr {
background-color: #dd0031;
color: #ffffff;
text-align: left;
}
}
tbody tr:hover {
background-color: #f6f6f6;
}
th,
td {
padding: 1rem;
}
}
`,
],
})
export class AppTableComponent {
@Input() data!: any[];
@ContentChild('headers') headers: TemplateRef<any> | undefined;
@ContentChild('rows') rows: TemplateRef<any> | undefined;
}
So what are we doing?
With the @Input() data
property we are passing in the data for the table which will allow our templates to access that data.
We are also using @ContentChild
to grab a reference to those templates. We use the names of the templates so that we can access those within those templates.
This is very similar to Angular's @ViewChild
which allows us to reference a child object in our template.
2. Using the template
And now we'll create a simple table component that we'll call inventory table.
ng generate component inventory-table
And our code.
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { AppTableComponent } from '../app-table/app-table.component';
@Component({
selector: 'app-inventory-table',
standalone: true,
imports: [CommonModule, AppTableComponent],
template: `
<app-table [data]="inventory">
<ng-template #headers>
<th>Item</th>
<th>Price</th>
<th></th>
<th></th>
</ng-template>
<ng-template #rows let-row>
<td>{{ row.name }}</td>
<td>{{ row.price | currency: row.currency }}</td>
<td>
<button *ngIf="row.inStock > 0" (click)="purchaseItem(row.plu)">
Buy now
</button>
</td>
<td>
<button>Delete</button>
</td>
</ng-template>
</app-table>
`,
})
export class AppInventoryComponent {
inventory = [
{
plu: 110,
supplier: 'X Corp',
name: 'Table extender',
inStock: 500,
price: 50,
currency: 'GBP',
},
{
plu: 120,
supplier: 'X Corp',
name: 'Heated toilet seat',
inStock: 0,
price: 80,
currency: 'GBP',
},
{
plu: 155,
supplier: 'Y Corp',
name: 'Really good pencil',
inStock: 1,
price: 8000,
currency: 'AUD',
},
];
purchaseItem(plu: number) {
console.log('handle purchase for', plu);
}
}
And we should a table that looks something like this.
Conclusion
You'll notice that ngTemplateOutlet
can prove itself very useful in situations where you would otherwise have to provide a lot of weird configurations for your component.
With ngTemplateOutlet the component itself remains a simple and generic component and all of the complexity is handled directly in the implementation component.
If you're interested in learning more about this I highly recommend Joshua Morony's video - ngTemplateOutlet is WAY more useful than I realised.
Till later,
Daniel Kreider