Un <ng-template> e o bucată de template care nu se randează singură: e o „rețetă” pe care o poți instanția oricând, de oricâte ori, cu date diferite. Combinat cu ngTemplateOutlet, îți permite să construiești componente care se ocupă de comportament (selecție, paginare, virtualizare) și lasă aspectul în seama părintelui — echivalentul „render props” din React.
Bazele: ng-template + ngTemplateOutlet + context
<!-- Un <ng-template> NU se randează singur: e o „rețetă” pe care o instanțiezi când vrei -->
<ng-template #greeting let-name let-time="time">
<p>Bună {{ time }}, {{ name }}!</p>
</ng-template>
<!-- îl instanțiezi cu ngTemplateOutlet, dându-i un context -->
<ng-container
[ngTemplateOutlet]="greeting"
[ngTemplateOutletContext]="{ $implicit: 'Ana', time: 'dimineața' }"
/>
<!-- $implicit → let-name ; time → let-time="time" -->Componentă cu template personalizabil
// O listă care se ocupă de selecție și navigare, dar lasă PĂRINTELE să decidă cum arată un rând.
@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 });
}- Mere
- Pere
- Prune
- AnaTech lead#1 selectat
- MihaiFrontend
- IoanaDesign
Context tipizat cu ngTemplateContextGuard
Fără ajutor, variabilele let-* sunt any — pierzi autocompletarea și verificarea de tipuri. O directivă mică pe ng-template, cu o metodă statică ngTemplateContextGuard, rezolvă asta. E exact tehnica folosită de NgFor, CDK Table și majoritatea librăriilor de UI.
// Fără ajutor, variabilele let-* dintr-un <ng-template> sunt `any`.
// O directivă cu ngTemplateContextGuard le dă tipul corect.
interface ItemContext<T> {
$implicit: T;
index: number;
selected: boolean;
}
@Directive({ selector: 'ng-template[appItem]' })
export class ItemTemplate<T> {
// input folosit doar ca TypeScript să deducă T din [appItem]="members()"
readonly appItem = input.required<T[]>();
static ngTemplateContextGuard<T>(_dir: ItemTemplate<T>, ctx: unknown): ctx is ItemContext<T> {
return true;
}
}Template ca input
// Alternativ: template-ul ca INPUT (când nu vrei proiecție)
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" />Injectorul folosit de outlet (v21.2)
// v21.2: ngTemplateOutlet poate folosi injectorul de la locul de randare (opțiunea 'outlet')
<ng-container
[ngTemplateOutlet]="tpl"
[ngTemplateOutletInjector]="'outlet'"
/>
// Implicit, un template „vede” injectorul de unde a fost DECLARAT.
// Cu 'outlet' vede injectorul componentei care îl RANDEAZĂ — util pentru template-uri
// care injectează un context oferit de container (ex. un serviciu de selecție).ng-container: grupare fără element
<ng-container> nu apare în DOM. Îl folosești când ai nevoie de un loc în template, dar nu vrei un <div> în plus: ca gazdă pentru ngTemplateOutlet, ca să grupezi câteva elemente pentru o directivă, sau acolo unde un element în plus ar strica HTML-ul (în <tr>, <ul>, <select>) ori layout-ul flex sau grid.
<!-- un element în plus în <select> ar fi HTML invalid; ng-container nu apare în 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>
<!-- gazdă pentru un template, fără <div> -->
<ng-container *ngTemplateOutlet="rowTpl(); context: { $implicit: row }" />O directivă structurală proprie
Steluța din *appIfRole e doar o prescurtare: Angular pune elementul într-un <ng-template> și dă directivei TemplateRef. Directiva decide singură când creează view-ul (createEmbeddedView) și când îl șterge (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); // creează conținutul doar când rolul se potrivește
}
});
}
}
<!-- folosire, cu steluță -->
<button *appIfRole="'admin'">Șterge</button>
<!-- ce face Angular din steluță -->
<ng-template [appIfRole]="'admin'"><button>Șterge</button></ng-template>În aplicații îți trebuie rar una: @if acoperă aproape tot. Merită când aceeași condiție, care depinde de un serviciu (rol, feature flag, limbă), se repetă în zeci de template-uri.
Când DA, când NU
- Componente generice reutilizabile: listă, tabel, select, autocomplete, tree.
- Conținut care trebuie creat lazy sau de mai multe ori (vezi capcana din Content projection).
- Sloturi „opționale” cu fallback: dacă părintele dă template, îl folosești; altfel afișarea implicită.
- Conținut static, afișat o singură dată →
<ng-content>e mai simplu. - Ai un singur loc de utilizare → scrie markup-ul direct, fără abstracție.
- Variații mici de aspect → un input (
variant,size) e mai clar.
Capcane frecvente
- Context nou la fiecare randare:
[ngTemplateOutletContext]="{ ... }"creează un obiect nou; Angular îl gestionează eficient, dar nu pune calcule grele în el. let-xfără valoare citește$implicit;let-x="cheie"citeștecontext.cheie.- Template declarat într-un loc, randat în altul: folosește injectorul de la declarare (vezi opțiunea
'outlet'din v21.2).
Fișă rapidă
<ng-template #tpl let-item let-i="index">…</ng-template> <!-- un fragment care nu se randează singur -->
<ng-container *ngTemplateOutlet="tpl; context: { $implicit: x, index: 0 }" />
<ng-container [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="ctx" />
<ng-container>…</ng-container> <!-- grupare fără element în DOM -->
readonly tpl = contentChild<TemplateRef<Ctx>>('row'); <!-- template primit prin proiecție -->
readonly tpl = input<TemplateRef<Ctx>>(); <!-- template primit ca input -->
inject(TemplateRef) inject(ViewContainerRef) <!-- directivă structurală proprie -->
vcr.createEmbeddedView(tpl, ctx) vcr.clear()