Define reusable template fragments, group elements without adding a wrapper, and render templates dynamically with ngTemplateOutlet.

Bishal Biswas
Self Learner
ng-container groups without adding an element
Sometimes you need a wrapper for Angular’s benefit but not in the DOM.
<!-- adds a pointless div to the page -->
<div *ngIf="visible">
<li>One</li>
<li>Two</li>
</div>
<!-- renders nothing itself -->
<ng-container *ngIf="visible">
<li>One</li>
<li>Two</li>
</ng-container>
<ng-container> disappears at runtime. Nothing is added to the DOM. That matters inside tables, lists and flex or grid layouts, where a stray div breaks the structure.
<table>
<tbody>
<ng-container *ngFor="let group of groups">
<tr class="group-header"><td>{{ group.name }}</td></tr>
<tr *ngFor="let row of group.rows"><td>{{ row.value }}</td></tr>
</ng-container>
</tbody>
</table>
Wrapping those rows in a div would produce invalid HTML. ng-container does not.
The other classic use is combining two structural directives, which cannot share an element:
<ng-container *ngFor="let item of items">
<li *ngIf="item.visible">{{ item.name }}</li>
</ng-container>
Control flow blocks made this less necessary
With @if and @for, most of those cases disappear.
@for (group of groups(); track group.id) {
<tr class="group-header"><td>{{ group.name }}</td></tr>
@for (row of group.rows; track row.id) {
<tr><td>{{ row.value }}</td></tr>
}
}
No wrapper needed at all, because blocks are not elements. ng-container is still useful with ngTemplateOutlet and with directives, but you will reach for it far less often.
ng-template defines, it does not render
<ng-template #greeting>
<p>Hello there</p>
</ng-template>
That renders nothing. ng-template defines a fragment of markup for something else to use later.
Three things use them: structural directives, ngTemplateOutlet, and components that accept a template as input.
ngTemplateOutlet
<ng-template #greeting>
<p>Hello there</p>
</ng-template>
<ng-container [ngTemplateOutlet]="greeting" />
<ng-container [ngTemplateOutlet]="greeting" />
Now it renders, twice. This is the way to reuse a fragment without creating a component.
Needs importing:
import { NgTemplateOutlet } from '@angular/common';
@Component({ imports: [NgTemplateOutlet], ... })
Passing data to a template
Templates take context, declared with let- bindings.
<ng-template #row let-user let-index="i">
<tr>
<td>{{ index }}</td>
<td>{{ user.name }}</td>
</tr>
</ng-template>
<ng-container
[ngTemplateOutlet]="row"
[ngTemplateOutletContext]="{ $implicit: currentUser, i: 0 }" />
$implicit is the default value, bound by a bare let-user. Other keys are bound by name with let-index="i".
This is exactly how *ngFor works internally, which is why let item of items looks the way it does.
Choosing a template at runtime
@Component({
selector: 'app-status',
imports: [NgTemplateOutlet],
template: `
<ng-template #loading><app-spinner /></ng-template>
<ng-template #error><p class="error">Failed</p></ng-template>
<ng-template #ready><app-results [data]="data()" /></ng-template>
<ng-container [ngTemplateOutlet]="templateFor()" />
`,
})
export class Status {
readonly state = input<'loading' | 'error' | 'ready'>('loading');
private readonly loading = viewChild.required<TemplateRef<unknown>>('loading');
private readonly error = viewChild.required<TemplateRef<unknown>>('error');
private readonly ready = viewChild.required<TemplateRef<unknown>>('ready');
protected readonly templateFor = computed(() => {
switch (this.state()) {
case 'loading': return this.loading();
case 'error': return this.error();
case 'ready': return this.ready();
}
});
}
An @switch block is simpler for this specific case. The template approach earns its keep when the choice is made somewhere else, or when templates are passed in from outside.
Accepting a template as input
This is the powerful pattern. A component takes markup from its caller and decides when and how often to render it.
@Component({
selector: 'app-data-table',
imports: [NgTemplateOutlet],
template: `
<table>
@for (row of rows(); track row.id) {
<ng-container
[ngTemplateOutlet]="rowTemplate()"
[ngTemplateOutletContext]="{ $implicit: row }" />
}
</table>
`,
})
export class DataTable<T extends { id: number }> {
readonly rows = input.required<T[]>();
readonly rowTemplate = input.required<TemplateRef<{ $implicit: T }>>();
}
<ng-template #userRow let-user>
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</ng-template>
<app-data-table [rows]="users()" [rowTemplate]="userRow" />
The table owns the looping, sorting and paging. The caller owns what a row looks like. Neither knows about the other’s concerns.
Nearly every serious Angular table and list library is built this way.
Getting a template with contentChild
Instead of an input, take the template from projected content.
@Component({
selector: 'app-repeater',
imports: [NgTemplateOutlet],
template: `
@for (item of items(); track $index) {
<ng-container
[ngTemplateOutlet]="itemTemplate()"
[ngTemplateOutletContext]="{ $implicit: item }" />
}
`,
})
export class Repeater {
readonly items = input.required<unknown[]>();
readonly itemTemplate = contentChild.required(TemplateRef);
}
<app-repeater [items]="products()">
<ng-template let-product>
<app-product-card [product]="product" />
</ng-template>
</app-repeater>
Slightly nicer to call, since there is no named reference to wire up.
Creating a view in code
For full control, use ViewContainerRef.
export class Manual {
private readonly container = inject(ViewContainerRef);
private readonly template = viewChild.required<TemplateRef<unknown>>('tpl');
show(): void {
this.container.createEmbeddedView(this.template(), { $implicit: 'data' });
}
clear(): void {
this.container.clear();
}
}
This is what structural directives do internally, and it is covered in the dynamic components topic.
ng-container or ng-template?
ng-container renders its children, adds no element
ng-template renders nothing until something asks for it
Use ng-container when you need a grouping that leaves no trace. Use ng-template when you want to define markup for later.
Quick recap
ng-containergroups elements without adding anything to the DOM.- It is essential inside tables and lists where a wrapper would be invalid.
- Control flow blocks removed most of the old need for it.
ng-templaterenders nothing on its own, it defines a fragment.ngTemplateOutletrenders a template, andngTemplateOutletContextpasses data.$implicitis bound by a barelet-x, other keys bylet-x="key".- Taking a
TemplateRefas an input or viacontentChildlets callers control rendering.
import { Component, input, signal, contentChild, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
// ng-container groups WITHOUT adding an element. Essential inside tables.
// <tbody>
// <ng-container *ngFor="let g of groups">
// <tr>...</tr>
// <tr *ngFor="let r of g.rows">...</tr>
// </ng-container>
// </tbody>
// A wrapping div there would be invalid HTML. ng-container leaves no trace.
// ng-template renders NOTHING on its own. It defines markup for later.
@Component({
selector: 'app-outlet-demo',
imports: [NgTemplateOutlet],
template: `
<ng-template #greeting><p>Hello there</p></ng-template>
<!-- now it renders, twice -->
<ng-container [ngTemplateOutlet]="greeting" />
<ng-container [ngTemplateOutlet]="greeting" />
<!-- $implicit binds to a bare let-x, named keys to let-x="key" -->
<ng-template #row let-user let-i="index">
<tr><td>{{ i }}</td><td>{{ user.name }}</td></tr>
</ng-template>
<ng-container
[ngTemplateOutlet]="row"
[ngTemplateOutletContext]="{ $implicit: { name: 'Asha' }, index: 0 }" />
`,
})
export class OutletDemo {}
// The powerful pattern: the component owns the LOOP, the caller owns the ROW.
@Component({
selector: 'app-repeater',
imports: [NgTemplateOutlet],
template: `
@for (item of items(); track $index) {
<ng-container
[ngTemplateOutlet]="itemTemplate()"
[ngTemplateOutletContext]="{ $implicit: item }" />
}
`,
})
export class Repeater {
readonly items = input.required<unknown[]>();
readonly itemTemplate = contentChild.required(TemplateRef);
}
// Called like this. Every serious table library works this way.
// <app-repeater [items]="products()">
// <ng-template let-product><app-product-card [product]="product" /></ng-template>
// </app-repeater>
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready