Front-end angularforall.com

- ng-template Angular : templates conditionnels

Angular Ng-Template Templateref Ngtemplateoutlet Viewcontainerref Contentchild Templates-Dynamiques Control-Flow Structural-Directive Context-Guard Design-System Viewmodel
ng-template Angular : templates conditionnels

ng-template Angular : ngTemplateOutlet, TemplateRef, ViewContainerRef, contexte type-safe, contentchild, control flow Angular 17 et patterns design system.

ng-template : rôle et fonctionnement

<ng-template> est une balise Angular qui définit un bloc de contenu non rendu par défaut. Elle sert de conteneur pour du contenu conditionnel, réutilisable ou créé dynamiquement.

Point clé : ng-template n'est jamais présent dans le DOM final. Il ne génère aucun élément HTML — c'est un blueprint que Angular utilise pour créer du contenu à la demande.

Comprendre la désucration des directives structurelles :

<!-- Syntaxe sucrée (shorthand) -->
<div *ngIf="condition">Contenu</div>

<!-- Désucré par Angular en interne -->
<ng-template [ngIf]="condition">
    <div>Contenu</div>
</ng-template>

<!-- Idem pour *ngFor -->
<li *ngFor="let item of items">{{ item }}</li>

<!-- Désucré en -->
<ng-template ngFor let-item [ngForOf]="items">
    <li>{{ item }}</li>
</ng-template>
ÉlémentDans le DOMUsage
ng-templateJamaisBlueprint conditionnel/dynamique
ng-containerJamaisGrouper sans créer d'élément
ng-contentSon contenu projetéContent projection (slots)
div (avec *ngIf)Oui si vraiÉlément conditionnel avec wrapper

Nouvelle syntaxe @if / @for (Angular 17+)

Angular 17 introduit une nouvelle syntaxe de flow control avec @if, @for et @switch. Plus lisible et plus performante que les directives *ngIf et *ngFor.

<!-- @if avec @else if et @else -->
@if (user.role === 'admin') {
    <admin-dashboard />
} @else if (user.role === 'editor') {
    <editor-panel />
} @else {
    <viewer-mode />
}

<!-- @for avec variable implicite $index, $first, $last, $even, $odd -->
@for (item of items; track item.id) {
    <div [class.first]="$first" [class.last]="$last">
        {{ $index + 1 }}. {{ item.name }}
    </div>
} @empty {
    <p>Aucun élément à afficher.</p>
}

<!-- @switch / @case / @default -->
@switch (status) {
    @case ('loading') { <mat-spinner /> }
    @case ('error')   { <error-state /> }
    @case ('empty')   { <empty-state /> }
    @default          { <data-table [data]="data" /> }
}
track est obligatoire avec @for : il remplace trackBy et est requis pour les performances. Utilise track item.id ou track $index si pas d'identifiant unique.

Avantages vs directives *ngIf / *ngFor

  • Pas besoin d'importer NgIf, NgFor, NgSwitch dans le composant
  • @empty dans @for remplace le pattern *ngIf="items.length === 0"
  • Compilation plus rapide (Ivy optimisé)
  • $index, $first, $last disponibles sans let-index="index"
  • Lisibilité proche des langages de templating modernes (Svelte, Vue 3)

ng-template avec *ngIf et then/else

La forme classique avec *ngIf — toujours utile pour les templates nommés réutilisables et les patterns then/else explicites.

<!-- Syntaxe then/else avec templates nommés -->
<div *ngIf="isLoggedIn; then loggedInTmpl; else guestTmpl"></div>

<ng-template #loggedInTmpl>
    <nav class="user-nav">
        <span>Bienvenue, {{ user.name }} !</span>
        <button (click)="logout()">Déconnexion</button>
    </nav>
</ng-template>

<ng-template #guestTmpl>
    <div class="guest-banner">
        <p>Vous n'êtes pas connecté.</p>
        <button mat-flat-button (click)="openLogin()">Connexion</button>
    </div>
</ng-template>

Pattern utile pour les états de chargement avec async pipe :

<!-- Gestion loading/error/data avec ng-template -->
<ng-container *ngIf="users$ | async as users; else loadingTmpl">
    <user-list [users]="users" />
</ng-container>

<ng-template #loadingTmpl>
    <div class="d-flex justify-content-center p-4">
        <mat-spinner diameter="40"></mat-spinner>
    </div>
</ng-template>

ng-container vs div avec *ngIf

Préfère ng-container comme wrapper d'ancrage — il ne crée aucun élément DOM, évitant les divs parasites qui cassent les layouts Flexbox/Grid.

Variables de template avec let

Le mot-clé let crée des variables locales dans le contexte d'un template. Indispensable pour éviter de répéter des expressions coûteuses.

<!-- let avec *ngIf + async — évite 3 souscriptions -->
<ng-container *ngIf="{
    user: user$ | async,
    config: config$ | async,
    permissions: permissions$ | async
} as vm">
    @if (vm.user && vm.config) {
        <app-dashboard
            [user]="vm.user"
            [config]="vm.config"
            [permissions]="vm.permissions"
        />
    }
</ng-container>

<!-- let dans un ngFor étendu -->
<ng-template ngFor let-product [ngForOf]="products" let-i="index" let-last="last">
    <product-card
        [product]="product"
        [position]="i + 1"
        [showDivider]="!last"
    />
</ng-template>

ViewModel pattern avec combineLatest

// composant.ts — ViewModel unique pour éviter les async pipes multiples
import { Component, inject } from '@angular/core';
import { combineLatest, map } from 'rxjs';
import { AsyncPipe, NgIf } from '@angular/common';
import { UserService } from './user.service';
import { ConfigService } from './config.service';

interface ViewModel {
    user: User;
    config: AppConfig;
    isAdmin: boolean;
}

@Component({
    standalone: true,
    imports: [AsyncPipe, NgIf],
    template: `
        <ng-container *ngIf="vm$ | async as vm">
            <!-- vm est toujours défini ici — plus de ?. partout -->
            <h1>{{ vm.user.name }}</h1>
            @if (vm.isAdmin) { <admin-panel /> }
        </ng-container>
    `
})
export class DashboardComponent {
    private userSvc = inject(UserService);
    private configSvc = inject(ConfigService);

    // Un seul Observable → une seule souscription async pipe
    vm$ = combineLatest([this.userSvc.user$, this.configSvc.config$]).pipe(
        map(([user, config]): ViewModel => ({
            user,
            config,
            isAdmin: user.roles.includes('admin'),
        }))
    );
}

ng-template avec *ngFor

La syntaxe desugared de *ngFor donne accès à toutes les variables de contexte et permet des patterns avancés comme les colonnes alternées.

<!-- Syntaxe complète ngFor desugared -->
<ng-template
    ngFor
    let-item
    [ngForOf]="products"
    [ngForTrackBy]="trackById"
    let-index="index"
    let-count="count"
    let-first="first"
    let-last="last"
    let-even="even"
    let-odd="odd"
>
    <div
        class="product-item"
        [class.first-item]="first"
        [class.last-item]="last"
        [class.alt-row]="odd"
    >
        <span class="badge">{{ index + 1 }}/{{ count }}</span>
        <product-card [product]="item" />
    </div>
</ng-template>
// Toujours fournir trackBy pour les performances
trackById = (index: number, item: Product) => item.id;
// Sans trackBy : Angular redétruit TOUS les composants à chaque changement
// Avec trackBy : seuls les éléments modifiés/ajoutés/supprimés sont recréés

ngTemplateOutlet — affichage dynamique

ngTemplateOutlet permet d'afficher un template à un endroit précis avec un contexte personnalisé. Idéal pour les composants configurables depuis l'extérieur.

<!-- Afficher un template avec contexte -->
<ng-container
    [ngTemplateOutlet]="customHeader || defaultHeader"
    [ngTemplateOutletContext]="{ $implicit: item, index: i }"
></ng-container>

<!-- Template par défaut (fallback) -->
<ng-template #defaultHeader let-item let-i="index">
    <h3>{{ i + 1 }}. {{ item.title }}</h3>
</ng-template>

<!-- Template personnalisé passé depuis le parent -->
<app-data-list [items]="products" [headerTemplate]="myCustomHeader"></app-data-list>

<ng-template #myCustomHeader let-product let-i="index">
    <div class="custom-header">
        <img [src]="product.thumbnail" />
        <h3>{{ product.name }}</h3>
    </div>
</ng-template>

Composant data-list avec template configurable

// data-list.component.ts — ngTemplateOutlet pattern
import { Component, input } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
    selector: 'app-data-list',
    standalone: true,
    imports: [NgTemplateOutlet],
    template: `
        @for (item of items(); track item.id; let i = $index) {
            <div class="list-item">
                @if (headerTemplate()) {
                    <!-- Template custom passé depuis le parent -->
                    <ng-container
                        [ngTemplateOutlet]="headerTemplate()!"
                        [ngTemplateOutletContext]="{ $implicit: item, index: i }"
                    />
                } @else {
                    <!-- Rendu par défaut -->
                    <h3>{{ item.title }}</h3>
                }
                <ng-content />
            </div>
        }
    `
})
export class DataListComponent<T extends { id: string | number; title: string }> {
    items = input.required<T[]>();
    headerTemplate = input<TemplateRef<any> | null>(null);
}

TemplateRef et ViewContainerRef

TemplateRef représente une référence vers un ng-template. ViewContainerRef permet de créer et insérer des vues dans le DOM de façon programmatique.

// dynamic-modal.directive.ts — Directive qui ouvre un ng-template en modal
import {
    Directive, Input, HostListener,
    TemplateRef, ViewContainerRef, EmbeddedViewRef
} from '@angular/core';

@Directive({
    selector: '[appModal]',
    standalone: true
})
export class ModalDirective {
    @Input('appModal') template!: TemplateRef<any>;
    @Input('appModalContext') context: Record<string, any> = {};

    private viewRef: EmbeddedViewRef<any> | null = null;

    constructor(private vcRef: ViewContainerRef) {}

    @HostListener('click')
    open(): void {
        if (this.viewRef) return; // déjà ouvert

        // Créer la vue depuis le template avec contexte
        this.viewRef = this.vcRef.createEmbeddedView(this.template, {
            $implicit: this.context,
            close: () => this.close() // passer la méthode close au template
        });
    }

    close(): void {
        this.viewRef?.destroy();
        this.viewRef = null;
    }
}

// Utilisation dans le template parent
// <button [appModal]="confirmTmpl" [appModalContext]="{ item: selectedItem }">
//     Supprimer
// </button>
//
// <ng-template #confirmTmpl let-ctx let-close="close">
//     <div class="modal-overlay">
//         <p>Supprimer {{ ctx.item.name }} ?</p>
//         <button (click)="delete(ctx.item); close()">Confirmer</button>
//         <button (click)="close()">Annuler</button>
//     </div>
// </ng-template>

@ContentChild avec TemplateRef

Combiner @ContentChild avec TemplateRef est le pattern fondamental pour créer des composants génériques et hautement configurables — tables, listes, accordéons.

// table.component.ts — Table générique avec templates custom
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
    selector: 'app-table',
    standalone: true,
    imports: [NgTemplateOutlet],
    template: `
        <table class="table table-hover">
            <thead>
                <tr>
                    @if (headerTemplate) {
                        <ng-container [ngTemplateOutlet]="headerTemplate" />
                    } @else {
                        @for (col of columns; track col.key) {
                            <th>{{ col.label }}</th>
                        }
                    }
                </tr>
            </thead>
            <tbody>
                @for (row of data; track row.id) {
                    <tr>
                        @if (rowTemplate) {
                            <!-- Row complètement custom -->
                            <ng-container
                                [ngTemplateOutlet]="rowTemplate"
                                [ngTemplateOutletContext]="{ $implicit: row }"
                            />
                        } @else {
                            @for (col of columns; track col.key) {
                                <td>{{ row[col.key] }}</td>
                            }
                        }
                    </tr>
                }
            </tbody>
        </table>
    `
})
export class TableComponent {
    @Input() data: any[] = [];
    @Input() columns: { key: string; label: string }[] = [];

    // Récupère les ng-template depuis le contenu projeté
    @ContentChild('header') headerTemplate?: TemplateRef<any>;
    @ContentChild('row')    rowTemplate?: TemplateRef<any>;
}

// Utilisation avec templates custom
// <app-table [data]="users" [columns]="userCols">
//
//     <ng-template #header>
//         <th>Avatar</th>
//         <th>Nom complet</th>
//         <th>Actions</th>
//     </ng-template>
//
//     <ng-template #row let-user>
//         <td><img [src]="user.avatar" class="avatar"></td>
//         <td>{{ user.firstName }} {{ user.lastName }}</td>
//         <td>
//             <button (click)="edit(user)">Modifier</button>
//             <button (click)="delete(user)">Supprimer</button>
//         </td>
//     </ng-template>
//
// </app-table>

Bonnes pratiques et performance

  • Angular 17+ : préfère @if/@for pour le nouveau code — plus rapide et plus lisible
  • trackBy obligatoire avec *ngFor et track obligatoire avec @for — évite la re-création complète de la liste
  • Utilise ng-container comme wrapper d'ancrage *ngIf/*ngFor — pas de div parasite dans le DOM
  • Nomme tes templates avec des suffixes clairs : #loadingTmpl, #errorTmpl, #emptyTmpl
  • Pattern ViewModel avec combineLatest + un seul async pipe — évite les souscriptions multiples
  • ngTemplateOutlet avec fallback || defaultTmpl pour les composants configurables
  • Pour les tables/listes génériques : @ContentChild + TemplateRef pour les customisations de rows
// Anti-pattern : async pipe répété → 3 souscriptions HTTP
<div *ngIf="user$ | async as user">
    <img [src]="(user$ | async)?.avatar">      <!-- 2ème souscription ! -->
    <span>{{ (user$ | async)?.name }}</span>   <!-- 3ème souscription ! -->
</div>

<!-- Bonne pratique : ViewModel unique -->
<ng-container *ngIf="user$ | async as user">  <!-- 1 seule souscription -->
    <img [src]="user.avatar">
    <span>{{ user.name }}</span>
</ng-container>

<!-- Encore mieux en Angular 17+ : @if avec async -->
@if (user$ | async; as user) {
    <img [src]="user.avatar">
    <span>{{ user.name }}</span>
}
Migration vers la nouvelle syntaxe : Angular fournit un schematic de migration automatique : ng generate @angular/core:control-flow. Il convertit tous les *ngIf, *ngFor et ngSwitch en @if, @for et @switch dans tout le projet.

ngTemplateOutlet en profondeur — passage de contexte typé

ngTemplateOutlet est l'outil qui transforme ng-template en composant réutilisable et configurable. Sa syntaxe complète accepte un contexte qui expose des variables locales accessibles dans le template via la syntaxe let-x. C'est le mécanisme qui permet aux composants de design system Angular (Material, CDK, PrimeNG) d'offrir des slots typés et flexibles.

Variables de contexte vs $implicit

Le contexte passé à ngTemplateOutlet est un objet dont chaque clé devient une variable accessible via let-x="cleDeLObjet". La clé spéciale $implicit est récupérée sans nom de clé : let-item reçoit automatiquement $implicit. C'est la convention pour la donnée principale du template, équivalent de la variable d'itération dans *ngFor.

// Contexte avec $implicit + clés nommées
<ng-container *ngTemplateOutlet="tpl;
                                  context: { $implicit: user, index: i, isLast: i === total - 1 }">
</ng-container>

// Le template extrait avec let-X (= $implicit) et let-x="clé"
<ng-template #tpl let-user let-index="index" let-isLast="isLast">
    <div [class.last]="isLast">
        {{ index + 1 }}. {{ user.name }}
    </div>
</ng-template>

Réutilisation du même template à plusieurs endroits

Un même TemplateRef peut être instancié plusieurs fois avec des contextes différents — utile pour les listes paginées affichées en haut et bas, les modales et toasts qui partagent un layout, ou les tooltips multiples sur une page.

<ng-template #errorBadge let-message>
    <span class="badge bg-danger">{{ message }}</span>
</ng-template>

@for (error of validationErrors; track error.field) {
    <ng-container *ngTemplateOutlet="errorBadge; context: { $implicit: error.message }"></ng-container>
}

<footer>
    <ng-container *ngTemplateOutlet="errorBadge; context: { $implicit: globalError }"></ng-container>
</footer>
<!-- Définition du template avec deux variables de contexte -->
<ng-template #userCard let-user let-isAdmin="isAdmin">
  <article class="card" [class.admin]="isAdmin">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    @if (isAdmin) { <span class="badge">Admin</span> }
  </article>
</ng-template>

<!-- Instanciation avec contexte explicite -->
<ng-container
  *ngTemplateOutlet="userCard; context: { $implicit: currentUser, isAdmin: true }">
</ng-container>

Sans typage explicite, les variables let-x sont de type any en mode strict TypeScript — vous perdez tous les bénéfices de l'autocomplete et de la vérification de types. La méthode statique ngTemplateContextGuard résout ce problème en informant le compilateur TypeScript du type exact du contexte passé. C'est le pattern utilisé en interne par NgForOf qui type automatiquement let-item selon le tableau passé en input. Pour vos propres directives template, l'investissement de cinq lignes supplémentaires se rentabilise immédiatement en confort de développement.

Typage TypeScript du contexte (Angular 14+)

// Avec une directive utilitaire — préserve les types dans le template
@Directive({
  selector: 'ng-template[appTypedTemplate]',
  standalone: true,
})
export class TypedTemplateDirective<T> {
  @Input('appTypedTemplate') value!: T;

  static ngTemplateContextGuard<T>(
    dir: TypedTemplateDirective<T>,
    ctx: unknown,
  ): ctx is { $implicit: T; isAdmin: boolean } {
    return true;
  }
}

// Usage — autocomplete TypeScript sur let-user
<ng-template
  [appTypedTemplate]="currentUser"
  let-user
  let-isAdmin="isAdmin">
  {{ user.name }} <!-- typé User automatiquement -->
</ng-template>

Le pattern ngTemplateContextGuard fonctionne aussi avec les composants qui consomment des templates via @ContentChild. Si votre composant app-list reçoit un template de ligne depuis le parent et que vous voulez que les bindings dans ce template soient correctement typés, exposez le type attendu via une directive utilitaire. Sans cette précaution, vos consommateurs perdront le bénéfice du typage strict et leur code applicatif se remplira de $any() ou de as User pour contourner le problème.

Pattern : composant Table avec template de cellule custom

// table.component.ts
@Component({
  selector: 'app-table',
  standalone: true,
  imports: [NgTemplateOutlet],
  template: `
    <table>
      <tbody>
        @for (row of rows; track row.id) {
          <tr>
            <td>
              <ng-container
                *ngTemplateOutlet="cellTemplate || defaultTpl;
                                  context: { $implicit: row }">
              </ng-container>
            </td>
          </tr>
        }
      </tbody>
    </table>

    <ng-template #defaultTpl let-row>{{ row.label }}</ng-template>
  `,
})
export class TableComponent {
  @Input() rows: { id: string; label: string }[] = [];
  @ContentChild('cellTpl') cellTemplate?: TemplateRef<unknown>;
}

// Consommation — l'hôte fournit son propre template de cellule
<app-table [rows]="items">
  <ng-template #cellTpl let-row>
    <strong>{{ row.label }}</strong>
    <small>(id: {{ row.id }})</small>
  </ng-template>
</app-table>

Rendu programmatique avec ViewContainerRef

Au-delà des directives template, vous pouvez instancier un ng-template programmatiquement via ViewContainerRef.createEmbeddedView(). C'est utile pour le rendu conditionnel complexe (state machines, wizards), le contenu dynamique injecté depuis un service, ou les cas où la position du template doit être calculée à l'exécution.

La différence pratique avec le déclaratif est subtile mais importante. Avec @if ou *ngIf, Angular crée et détruit la vue en suivant le cycle de change detection — vous n'avez aucun contrôle direct, juste une expression booléenne. Avec ViewContainerRef, c'est vous qui décidez quand instancier la vue, où la placer dans le DOM, quelles données lui fournir, et quand la détruire. Le contrôle est total, mais vous portez aussi la responsabilité du cleanup et de la cohérence de l'état.

@Component({
  selector: 'app-stage',
  template: `
    <ng-template #step1 let-data>
      <h2>Bienvenue {{ data.user }}</h2>
      <button (click)="next()">Suivant</button>
    </ng-template>

    <ng-template #step2 let-data>
      <h2>Choisis ton plan</h2>
      <button (click)="next()">Continuer</button>
    </ng-template>

    <div #host></div>
  `,
})
export class StageComponent implements AfterViewInit {
  @ViewChild('step1') step1!: TemplateRef<unknown>;
  @ViewChild('step2') step2!: TemplateRef<unknown>;
  @ViewChild('host', { read: ViewContainerRef }) host!: ViewContainerRef;

  private step = 1;

  ngAfterViewInit() {
    this.render();
  }

  next() {
    this.step++;
    this.render();
  }

  private render() {
    this.host.clear();
    const tpl = this.step === 1 ? this.step1 : this.step2;
    this.host.createEmbeddedView(tpl, { $implicit: { user: 'Alice' } });
  }
}

Le bloc render() ci-dessus illustre la mécanique fondamentale : on clear l'hôte (this.host.clear()) pour détruire la vue précédente, puis on crée une nouvelle vue depuis le template choisi avec son propre contexte. Sans le clear préalable, les vues s'accumuleraient dans le DOM — chaque clic sur « Suivant » ajouterait une étape de plus à l'écran au lieu de remplacer l'existante. C'est l'erreur classique du rendu programmatique débutant.

Quand utiliser le rendu programmatique vs déclaratif

  • Déclaratif (@if, @for, ngTemplateOutlet) : 95 % des cas. Plus lisible, plus simple à maintenir, change detection optimisée.
  • Programmatique (ViewContainerRef) : portails, services qui injectent du contenu (notifications, modals globaux), libs UI bas niveau (CDK Overlay).

Concrètement, vous utiliserez le rendu programmatique dans deux scénarios très spécifiques. Premièrement, un service de notifications globales qui doit pouvoir injecter un template dans n'importe quel composant racine, depuis n'importe où dans le code. Le service récupère un ViewContainerRef via ApplicationRef et instancie le template à la volée. Deuxièmement, un composant de wizard ou state machine où la séquence d'étapes est calculée à l'exécution selon les choix de l'utilisateur — l'enchaînement déclaratif via @switch devient trop verbeux quand on a 10+ étapes conditionnelles. Pour tout le reste, restez en déclaratif.

Les composants du CDK Angular (Portal, Overlay, Dialog) sont entièrement construits sur ce pattern programmatique. Lire leur code source est un excellent moyen d'apprendre les subtilités de ViewContainerRef et TemplatePortal en production réelle. C'est aussi ce qui rend Angular Material si puissant pour construire des UX avancées sans réinventer la roue.

Conclusion

ng-template est la primitive la plus polyvalente du système de templates Angular. Elle alimente quatre patterns qu'on retrouve dans toute application professionnelle : les directives structurelles (*ngIf, *ngFor), les slots configurables (ngTemplateOutlet avec @ContentChild), le rendu programmatique (ViewContainerRef), et les templates de fallback (@if … @else avec template référencé).

Avec le nouveau control flow d'Angular 17+ (@if, @for, @switch), beaucoup d'usages basiques de ng-template deviennent obsolètes — c'est tant mieux, la nouvelle syntaxe est plus lisible. Mais ngTemplateOutlet, ViewContainerRef et les templates passés en @ContentChild restent indispensables pour construire des composants de design system réutilisables et configurables. Comprendre ces patterns vous fait passer de consommateur à concepteur de composants Angular.

Différences clés avec @ContentChild et @ContentChildren

@ContentChild(TemplateRef) retourne une référence au premier ng-template projeté. @ContentChildren(TemplateRef) retourne une QueryList de toutes les références. La différence cruciale : avec @ContentChild, le template est résolu une fois après ngAfterContentInit et reste stable. Avec @ContentChildren, la QueryList émet un changes Observable quand des templates sont ajoutés/retirés dynamiquement (via @if ou @for autour des templates projetés).

@Component({
  selector: 'app-tabs',
  template: `<ng-content></ng-content>
              @for (tpl of templates; track $index) {
                  <div class="tab-content">
                      <ng-container *ngTemplateOutlet="tpl"></ng-container>
                  </div>
              }`,
})
export class TabsComponent implements AfterContentInit {
    @ContentChildren(TemplateRef) templateList!: QueryList<TemplateRef<unknown>>;
    templates: TemplateRef<unknown>[] = [];

    ngAfterContentInit() {
        this.templates = this.templateList.toArray();
        // Réagir aux changements dynamiques
        this.templateList.changes.subscribe(() => {
            this.templates = this.templateList.toArray();
        });
    }
}

Depuis Angular 17, les signal queries (contentChild(), contentChildren()) remplacent ces décorateurs. La version signal expose un signal lecture-seule qui se met à jour automatiquement, sans QueryList ni Observable explicite.

Signal queries Angular 17+ — contentChild et contentChildren

// Signal queries remplacent @ContentChild dans Angular 17+
import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core';

@Component({
    selector: 'app-list',
    template: `
        @for (item of items; track item.id) {
            <ng-container *ngTemplateOutlet="rowTpl(); context: { $implicit: item }"></ng-container>
        }
    `,
})
export class ListComponent {
    rowTpl = contentChild.required<TemplateRef<unknown>>('rowTpl');
    // Signal lecture seule — réactif aux changements de template
}

NgTemplateOutletInjector — injection de dépendances scope-locales

Depuis Angular 14.1, ngTemplateOutletInjector permet de passer un injecteur custom au template instancié. Le template peut alors injecter des services différents de ceux du contexte parent — utile pour fournir un service scopé spécifique à un sous-arbre sans créer de composant intermédiaire.

const customInjector = Injector.create({
    providers: [{ provide: ThemeService, useValue: darkTheme }],
    parent: this.injector,
});

// Dans le template
<ng-container *ngTemplateOutlet="myTemplate;
                                  injector: customInjector;
                                  context: { $implicit: data }"></ng-container>
Récapitulatif :
  • Préférer @if/@for/@switch (Angular 17+) aux *ngIf/*ngFor en code applicatif
  • Garder ng-template + ngTemplateOutlet pour les slots configurables et les libs UI
  • Typer le contexte des templates via ngTemplateContextGuard
  • Utiliser ViewContainerRef pour les rendus programmatiques (portals, notifications)
  • Pattern ViewModel avec un seul async pipe pour éviter les souscriptions multiples
  • Nommer les templates clairement : #loadingTpl, #errorTpl, #cellTpl
  • Combiner @ContentChild + TemplateRef pour les composants de table/liste génériques
  • Migrer automatiquement avec ng generate @angular/core:control-flow

Investir du temps pour comprendre ng-template et son écosystème paie sur le long terme. Sur les six prochains mois de votre carrière Angular, vous consommerez et créerez des dizaines de composants génériques — chacun bénéficiera des patterns vus ici. Le control flow Angular 17+ simplifie le code applicatif quotidien, mais c'est ng-template et ses APIs qui vous permettent de construire votre propre couche d'abstraction réutilisable.

Partager