Un <ng-template> est un morceau de template qui ne s’affiche pas tout seul : c’est une « recette » que vous pouvez instancier à tout moment, autant de fois que vous voulez, avec des données différentes. Combiné à ngTemplateOutlet, il permet de construire des composants qui gèrent le comportement (sélection, pagination, virtualisation) et laissent l’apparence au parent — l’équivalent des « render props » de React.
Les bases : ng-template + ngTemplateOutlet + context
<!-- Un <ng-template> ne s’affiche PAS tout seul : c’est une « recette » que vous instanciez quand vous voulez -->
<ng-template #greeting let-name let-time="time">
<p>{{ name }}, bonne {{ time }} !</p>
</ng-template>
<!-- instanciez-le avec ngTemplateOutlet en lui donnant un context -->
<ng-container
[ngTemplateOutlet]="greeting"
[ngTemplateOutletContext]="{ $implicit: 'Ana', time: 'matinée' }"
/>
<!-- $implicit → let-name ; time → let-time="time" -->Un composant avec un template personnalisable
// Une liste qui gère la sélection et la navigation, mais laisse le PARENT décider de l’apparence d’une ligne.
@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 });
}- Pommes
- Poires
- Prunes
- AnaTech lead#1 sélectionné
- MihaiFrontend
- IoanaDesign
Context typé avec ngTemplateContextGuard
Sans aide, les variables let-* sont de type any — vous perdez l’autocomplétion et la vérification de types. Une petite directive sur le ng-template, avec une méthode statique ngTemplateContextGuard, règle le problème. C’est exactement la technique utilisée par NgFor, la CDK Table et la plupart des bibliothèques d’UI.
// Sans aide, les variables let-* d’un <ng-template> sont de type `any`.
// Une directive avec ngTemplateContextGuard leur donne le bon type.
interface ItemContext<T> {
$implicit: T;
index: number;
selected: boolean;
}
@Directive({ selector: 'ng-template[appItem]' })
export class ItemTemplate<T> {
// input utilisé uniquement pour que TypeScript puisse inférer T à partir de [appItem]="members()"
readonly appItem = input.required<T[]>();
static ngTemplateContextGuard<T>(_dir: ItemTemplate<T>, ctx: unknown): ctx is ItemContext<T> {
return true;
}
}Template en input
// Autre option : le template en INPUT (quand vous ne voulez pas de projection)
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" />L’injecteur utilisé par l’outlet (v21.2)
// v21.2: ngTemplateOutlet peut utiliser l’injecteur de l’endroit où il est AFFICHÉ (l’option 'outlet')
<ng-container
[ngTemplateOutlet]="tpl"
[ngTemplateOutletInjector]="'outlet'"
/>
// Par défaut, un template « voit » l’injecteur de l’endroit où il a été DÉCLARÉ.
// Avec 'outlet', il voit l’injecteur du composant qui l’AFFICHE — utile pour les templates
// qui injectent un context fourni par le conteneur (p. ex. un service de sélection).ng-container : regrouper sans élément
<ng-container> n’apparaît pas dans le DOM. On l’utilise quand on a besoin d’un emplacement dans le template sans <div> supplémentaire : comme hôte de ngTemplateOutlet, pour regrouper quelques éléments pour une directive, ou là où un élément de plus casserait le HTML (dans <tr>, <ul>, <select>) ou une mise en page flex ou grid.
<!-- un élément de plus dans <select> serait du HTML invalide ; ng-container n’apparaît pas dans le 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>
<!-- un hôte pour un template, sans <div> -->
<ng-container *ngTemplateOutlet="rowTpl(); context: { $implicit: row }" />Une directive structurelle maison
L’astérisque de *appIfRole n’est qu’un raccourci : Angular enveloppe l’élément dans un <ng-template> et donne à la directive un TemplateRef. La directive décide seule quand créer la vue (createEmbeddedView) et quand la retirer (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); // crée le contenu seulement quand le rôle correspond
}
});
}
}
<!-- utilisation, avec l’astérisque -->
<button *appIfRole="'admin'">Supprimer</button>
<!-- ce qu’Angular fait de l’astérisque -->
<ng-template [appIfRole]="'admin'"><button>Supprimer</button></ng-template>Dans une application, on en a rarement besoin : @if couvre presque tout. Cela vaut la peine quand la même condition, dépendant d’un service (rôle, feature flag, langue), se répète dans des dizaines de templates.
Quand OUI, quand NON
- Composants génériques réutilisables : liste, table, select, autocomplete, arbre.
- Du contenu qui doit être créé à la demande ou plusieurs fois (voir le piège dans Content projection).
- Des slots « optionnels » avec un repli : si le parent fournit un template, vous l’utilisez ; sinon, l’affichage par défaut.
- Contenu statique, affiché une seule fois →
<ng-content>est plus simple. - Vous n’avez qu’un seul endroit d’utilisation → écrivez directement le markup, sans abstraction.
- De petites variations d’apparence → un input (
variant,size) est plus clair.
Pièges fréquents
- Un nouveau context à chaque rendu :
[ngTemplateOutletContext]="{ ... }"crée un nouvel objet ; Angular le gère efficacement, mais n’y mettez pas de calculs lourds. let-xsans valeur lit$implicit;let-x="key"litcontext.key.- Un template déclaré à un endroit et affiché à un autre : utilisez l’injecteur de la déclaration (voir l’option
'outlet'depuis la v21.2).
Aide-mémoire
<ng-template #tpl let-item let-i="index">…</ng-template> <!-- un fragment qui ne s’affiche pas tout seul -->
<ng-container *ngTemplateOutlet="tpl; context: { $implicit: x, index: 0 }" />
<ng-container [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="ctx" />
<ng-container>…</ng-container> <!-- regroupement sans élément dans le DOM -->
readonly tpl = contentChild<TemplateRef<Ctx>>('row'); <!-- template reçu par projection -->
readonly tpl = input<TemplateRef<Ctx>>(); <!-- template reçu en input -->
inject(TemplateRef) inject(ViewContainerRef) <!-- directive structurelle maison -->
vcr.createEmbeddedView(tpl, ctx) vcr.clear()