Ein <ng-template> ist ein Stück Template, das sich nicht von selbst rendert: ein „Rezept“, das du jederzeit, beliebig oft und mit unterschiedlichen Daten instanziieren kannst. In Kombination mit ngTemplateOutlet kannst du Komponenten bauen, die sich um das Verhalten kümmern (Auswahl, Paginierung, Virtualisierung) und das Aussehen dem Parent überlassen — das Pendant zu „Render Props“ in React.
Die Grundlagen: ng-template + ngTemplateOutlet + Context
<!-- Ein <ng-template> rendert sich NICHT von selbst: Es ist ein „Rezept“, das du instanziierst, wann du willst -->
<ng-template #greeting let-name let-time="time">
<p>Guten {{ time }}, {{ name }}!</p>
</ng-template>
<!-- instanziiere es mit ngTemplateOutlet und gib ihm einen Context -->
<ng-container
[ngTemplateOutlet]="greeting"
[ngTemplateOutletContext]="{ $implicit: 'Ana', time: 'Morgen' }"
/>
<!-- $implicit → let-name ; time → let-time="time" -->Eine Komponente mit anpassbarem Template
// Eine Liste, die Auswahl und Navigation übernimmt, aber den PARENT entscheiden lässt, wie eine Zeile aussieht.
@Component({
selector: 'app-select-list',
imports: [NgTemplateOutlet],
template: `
@for (item of items(); track $index; let i = $index) {
<li [class.sel]="i === selectedIndex()" (click)="selectedIndex.set(i)">
@if (itemTpl(); as tpl) {
<ng-container
[ngTemplateOutlet]="tpl"
[ngTemplateOutletContext]="{ $implicit: item, index: i, selected: i === selectedIndex() }"
/>
} @else {
{{ item }}
}
</li>
}
`,
})
export class SelectList<T> {
readonly items = input.required<T[]>();
protected readonly selectedIndex = signal(0);
protected readonly itemTpl = contentChild(ItemTemplate, { read: TemplateRef });
}- Äpfel
- Birnen
- Pflaumen
- AnaTech Lead#1 ausgewählt
- MihaiFrontend
- IoanaDesign
Typisierter Context mit ngTemplateContextGuard
Ohne Hilfe sind die let-*-Variablen vom Typ any — du verlierst Autovervollständigung und Typprüfung. Eine kleine Direktive auf dem ng-template mit einer statischen ngTemplateContextGuard-Methode löst das. Es ist genau die Technik, die NgFor, die CDK Table und die meisten UI-Bibliotheken verwenden.
// Ohne Hilfe sind die let-*-Variablen eines <ng-template> vom Typ `any`.
// Eine Direktive mit ngTemplateContextGuard gibt ihnen den richtigen Typ.
interface ItemContext<T> {
$implicit: T;
index: number;
selected: boolean;
}
@Directive({ selector: 'ng-template[appItem]' })
export class ItemTemplate<T> {
// Input nur dafür, dass TypeScript T aus [appItem]="members()" ableiten kann
readonly appItem = input.required<T[]>();
static ngTemplateContextGuard<T>(_dir: ItemTemplate<T>, ctx: unknown): ctx is ItemContext<T> {
return true;
}
}Template als Input
// Alternativ: das Template als INPUT (wenn du keine Projektion willst)
export class DataGrid<T> {
readonly rows = input.required<T[]>();
readonly rowTemplate = input<TemplateRef<{ $implicit: T }>>();
}
// <ng-template #row let-user>{{ user.name }}</ng-template>
// <app-data-grid [rows]="users()" [rowTemplate]="row" />Der vom Outlet verwendete Injector (v21.2)
// v21.2: ngTemplateOutlet kann den Injector der Stelle nutzen, an der es GERENDERT wird (die Option 'outlet')
<ng-container
[ngTemplateOutlet]="tpl"
[ngTemplateOutletInjector]="'outlet'"
/>
// Standardmäßig „sieht“ ein Template den Injector der Stelle, an der es DEKLARIERT wurde.
// Mit 'outlet' sieht es den Injector der Komponente, die es RENDERT — nützlich für Templates,
// die einen vom Container bereitgestellten Context injizieren (z. B. einen Auswahl-Service).ng-container: Gruppieren ohne Element
<ng-container> erscheint nicht im DOM. Du nutzt es, wenn du einen Platz im Template brauchst, aber kein zusätzliches <div> willst: als Host für ngTemplateOutlet, um einige Elemente für eine Direktive zu gruppieren, oder wo ein zusätzliches Element das HTML (in <tr>, <ul>, <select>) oder ein Flex- bzw. Grid-Layout zerstören würde.
<!-- ein zusätzliches Element in <select> wäre ungültiges HTML; ng-container erscheint nicht im DOM -->
<select>
@for (group of groups(); track group.id) {
<ng-container>
<option disabled>{{ group.name }}</option>
@for (o of group.options; track o.id) {
<option [value]="o.id">{{ o.label }}</option>
}
</ng-container>
}
</select>
<!-- Host für ein Template, ohne <div> -->
<ng-container *ngTemplateOutlet="rowTpl(); context: { $implicit: row }" />Eine eigene strukturelle Direktive
Das Sternchen in *appIfRole ist nur eine Kurzform: Angular packt das Element in ein <ng-template> und gibt der Direktive ein TemplateRef. Die Direktive entscheidet selbst, wann sie den View erzeugt (createEmbeddedView) und wann sie ihn entfernt (clear).
@Directive({ selector: '[appIfRole]' })
export class IfRole {
private readonly tpl = inject(TemplateRef);
private readonly vcr = inject(ViewContainerRef);
private readonly auth = inject(AuthStore);
readonly appIfRole = input.required<Role>();
constructor() {
effect(() => {
this.vcr.clear();
if (this.auth.hasRole(this.appIfRole())) {
this.vcr.createEmbeddedView(this.tpl); // erzeugt den Inhalt nur, wenn die Rolle passt
}
});
}
}
<!-- Verwendung, mit Sternchen -->
<button *appIfRole="'admin'">Löschen</button>
<!-- was Angular aus dem Sternchen macht -->
<ng-template [appIfRole]="'admin'"><button>Löschen</button></ng-template>In Apps brauchst du selten eine: @if deckt fast alles ab. Sie lohnt sich, wenn dieselbe Bedingung, die von einem Service abhängt (Rolle, Feature-Flag, Sprache), in Dutzenden Templates wiederkehrt.
Wann ja, wann nein
- Wiederverwendbare generische Komponenten: Liste, Tabelle, Select, Autocomplete, Tree.
- Inhalt, der lazy oder mehrfach erzeugt werden muss (siehe die Falle unter Content Projection).
- „Optionale“ Slots mit Fallback: Gibt der Parent ein Template mit, nutzt du es; sonst die Standardanzeige.
- Statischer Inhalt, der einmal angezeigt wird →
<ng-content>ist einfacher. - Es gibt nur eine Verwendungsstelle → schreibe das Markup direkt, ohne Abstraktion.
- Kleine Variationen im Aussehen → ein Input (
variant,size) ist klarer.
Häufige Stolperfallen
- Ein neuer Context bei jedem Rendern:
[ngTemplateOutletContext]="{ ... }"erzeugt ein neues Objekt; Angular geht effizient damit um, aber packe keine aufwendigen Berechnungen hinein. let-xohne Wert liest$implicit;let-x="key"liestcontext.key.- Ein Template, das an einer Stelle deklariert und an einer anderen gerendert wird: Nutze den Injector von der Deklaration (siehe die Option
'outlet'ab v21.2).
Spickzettel
<ng-template #tpl let-item let-i="index">…</ng-template> <!-- ein Fragment, das sich nicht selbst rendert -->
<ng-container *ngTemplateOutlet="tpl; context: { $implicit: x, index: 0 }" />
<ng-container [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="ctx" />
<ng-container>…</ng-container> <!-- Gruppieren ohne Element im DOM -->
readonly tpl = contentChild<TemplateRef<Ctx>>('row'); <!-- Template per Projektion erhalten -->
readonly tpl = input<TemplateRef<Ctx>>(); <!-- Template als Input erhalten -->
inject(TemplateRef) inject(ViewContainerRef) <!-- eigene strukturelle Direktive -->
vcr.createEmbeddedView(tpl, ctx) vcr.clear()