Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Les directives ngIf, ngFor, ngSwitch et le control flow @if, @for, @switch

Cette leçon compare les anciennes directives structurelles *ngIf, *ngFor et [ngSwitch], dépréciées depuis Angular 20, aux blocs de contrôle @if, @for et @switch. Elle couvre @else if, l’alias as et le...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Cette leçon compare les anciennes directives structurelles *ngIf, *ngFor et [ngSwitch], dépréciées depuis Angular 20, aux blocs de contrôle @if, @for et @switch. Elle couvre @else if, l’alias as et les variables $index, $first, $last et $count. Elle présente aussi @empty, le choix de l’expression track, les @case multiples, le @switch exhaustif avec @default never et les variables @let. Elle se termine par la migration automatique d’un projet, sept activités corrigées et les erreurs fréquentes.
  1. Objectifs

    • Lire et comprendre un template écrit avec *ngIf, *ngFor et [ngSwitch] (code existant)
    • Écrire des conditions avec @if, @else if, @else et l'alias as
    • Écrire des boucles avec @for, choisir la bonne expression track et utiliser $index, $first, $last, $even, $count, @empty
    • Choisir un affichage avec @switch, regrouper des cas et rendre le choix exhaustif
    • Déclarer des variables de template avec @let
    • Migrer automatiquement un projet vers le control flow
  1. Présentation

    • Un template doit souvent afficher ou masquer un élément, répéter un élément pour chaque valeur d'une liste ou choisir un affichage parmi plusieurs. Pendant des années, Angular a confié ces trois tâches à des directives structurelles : *ngIf, *ngFor et [ngSwitch].
    • Depuis Angular 17, le langage des templates possède ses propres blocs de contrôle (le control flow) : @if, @for et @switch, complétés par @let en Angular 18.1. Depuis Angular 20, NgIf, NgFor et NgSwitch sont dépréciées: elles fonctionnent encore, mais tout nouveau code utilise les blocs.
    • Cette leçon présente les deux écritures côte à côte : les directives pour lire le code existant, les blocs pour écrire le vôtre.
  • Pour suivre ce chapitre, continuez dans le projet premiers-pas-angular et créez un dossier src/app/controle/. Tous les exemples ont été compilés et testés avec Angular 22.2 (composants OnPush par défaut, sans Zone.js).

Évolution : des directives *ngIf, *ngFor, [ngSwitch] aux blocs @if, @for, @switch

  1. La directive *ngIf

    • *ngIf affiche son élément quand l'expression est vraie et le retire du DOM sinon (il n'est pas simplement caché). Elle doit être importée (NgIf depuis @angular/common).
    • Le cas « sinon » passe par un <ng-template> nommé (; else connexion), et as u mémorise la valeur testée. Avec then et else, les deux branches sont des modèles séparés :
import { Component, signal } from '@angular/core';
import { NgIf } from '@angular/common';

interface Utilisateur { nom: string; role: 'eleve' | 'formateur'; }

// *ngIf : la directive historique (dépréciée depuis Angular 20), avec else, then et as
@Component({
  selector: 'app-ngif',
  imports: [NgIf],
  template: `
    <button (click)="basculer()">{{ utilisateur() ? 'Se déconnecter' : 'Se connecter' }}</button>

    <p *ngIf="utilisateur() as u; else connexion">Bonjour {{ u.nom }} ({{ u.role }})</p>
    <ng-template #connexion><p class="gris">Vous n'êtes pas connecté.</p></ng-template>

    <ng-container *ngIf="utilisateur()?.role === 'formateur'; then outils; else rien" />
    <ng-template #outils><p>🛠 Outils du formateur</p></ng-template>
    <ng-template #rien></ng-template>
  `,
  styles: `.gris { color: #64748b; }`,
})
export class NgIfExemple {
  protected readonly utilisateur = signal<Utilisateur | null>(null);
  protected basculer() {
    this.utilisateur.set(this.utilisateur() ? null : { nom: 'Amel', role: 'formateur' });
  }
}

Résultat de *ngIf : utilisateur connecté et outils du formateur

  • Oublier l'import de NgIf ne bloque pas la compilation : Angular affiche un avertissement, puis l'erreur NG0303: Can't bind to 'ngIf' since it isn't a known property of 'p' dans la console, et le paragraphe n'apparaît jamais.

Avertissement NG8103 : *ngIf utilisé sans importer NgIf

  1. Le bloc @if

    • @if (condition) { … } fait la même chose, sans import. Il accepte @else if (autant que nécessaire) et @else, et l'alias s'écrit après un point-virgule : @if (utilisateur(); as u).
    • Les blocs s'imbriquent librement :
import { Component, signal } from '@angular/core';

interface Utilisateur { nom: string; role: 'eleve' | 'formateur'; }

// @if : le bloc de contrôle, avec @else if, @else et as
@Component({
  selector: 'app-si',
  template: `
    <button (click)="basculer()">{{ utilisateur() ? 'Se déconnecter' : 'Se connecter' }}</button>
    <label>Note : <input type="number" min="0" max="20" [value]="note()" (input)="note.set(+$any($event.target).value)" /></label>

    @if (utilisateur(); as u) {
      <p>Bonjour {{ u.nom }} ({{ u.role }})</p>
      @if (u.role === 'formateur') {
        <p>🛠 Outils du formateur</p>
      }
    } @else {
      <p class="gris">Vous n'êtes pas connecté.</p>
    }

    @if (note() >= 16) {
      <p class="tb">Très bien</p>
    } @else if (note() >= 10) {
      <p class="ok">Admis</p>
    } @else {
      <p class="ko">Ajourné</p>
    }
  `,
  styles: `.gris { color: #64748b; } .tb { color: #16a34a; font-weight: 700; } .ok { color: #3178c6; } .ko { color: #dc2626; } input { width: 60px; }`,
})
export class Si {
  protected readonly utilisateur = signal<Utilisateur | null>(null);
  protected readonly note = signal(12);
  protected basculer() {
    this.utilisateur.set(this.utilisateur() ? null : { nom: 'Amel', role: 'formateur' });
  }
}

Résultat de @if : bonjour Amel et la mention Admis

  • À l'intérieur du bloc, TypeScript sait que u n'est pas null: u.nom est accepté sans ?..
  • Pour écrire un @, une { ou une } comme simple texte dans un template, utilisez les entités &#64;, &#123; et &#125;.

Correspondance entre *ngIf et @if

  1. La directive *ngFor

    • *ngFor="let c of cours()" répète l'élément pour chaque valeur. Elle expose des variables locales que l'on renomme avec let: index, first, last, even, odd, count. trackBy reçoit une fonction qui identifie chaque élément.
    • Un élément ne peut porter qu'une seule directive précédée de *: pour combiner une boucle et une condition, il faut un <ng-container>, qui n'ajoute aucune balise au DOM.
import { Component, signal } from '@angular/core';
import { NgFor, NgIf } from '@angular/common';

interface Cours { id: number; titre: string; gratuit: boolean; }

// *ngFor : index, first, last, even, odd, count, et trackBy
@Component({
  selector: 'app-ngfor',
  imports: [NgFor, NgIf],
  template: `
    <ul>
      <li *ngFor="let c of cours(); let i = index; let premier = first; let dernier = last; let pair = even; let total = count; trackBy: parId"
          [class.pair]="pair">
        {{ i + 1 }}/{{ total }} · {{ c.titre }}
        <b *ngIf="premier">(nouveau)</b><b *ngIf="dernier">(dernier)</b>
      </li>
    </ul>

    <!-- Boucle + condition : il faut un <ng-container> (une seule directive * par élément) -->
    <p>Gratuits :
      <ng-container *ngFor="let c of cours(); trackBy: parId">
        <span *ngIf="c.gratuit" class="badge">{{ c.titre }}</span>
      </ng-container>
    </p>
  `,
  styles: `ul { margin: 4px 0; padding-left: 18px; } .pair { background: #f1f5f9; } b { color: #3178c6; margin-left: 6px; font-size: 12px; } .badge { background: #dcfce7; padding: 1px 8px; border-radius: 10px; margin-right: 4px; }`,
})
export class NgForExemple {
  protected readonly cours = signal<Cours[]>([
    { id: 1, titre: 'TypeScript', gratuit: true },
    { id: 2, titre: 'Angular', gratuit: false },
    { id: 3, titre: 'Signals', gratuit: true },
  ]);
  protected parId(_index: number, c: Cours) {
    return c.id;
  }
}

Résultat de *ngFor : liste numérotée et cours gratuits

  1. Le bloc @for

    • @for (c of cours(); track c.id) { … } remplace *ngFor. L'expression track est obligatoire: sans elle, la compilation échoue.
    • Les variables sont intégrées et préfixées par $: $index, $first, $last, $even, $odd, $count. On peut les utiliser telles quelles ou les renommer (let i = $index), ce qui est utile dans des boucles imbriquées.
    • Le bloc @empty s'affiche quand la liste est vide :
import { Component, signal } from '@angular/core';

interface Cours { id: number; titre: string; gratuit: boolean; }

// @for : track obligatoire, variables $index, $first, $last, $even, $count, et @empty
@Component({
  selector: 'app-pour',
  template: `
    <button (click)="ajouter()">Ajouter</button>
    <button (click)="vider()">Vider</button>
    <ul>
      @for (c of cours(); track c.id; let i = $index, premier = $first, dernier = $last) {
        <li [class.pair]="$even">
          {{ i + 1 }}/{{ $count }} · {{ c.titre }}
          @if (premier) { <b>(nouveau)</b> }
          @if (dernier) { <b>(dernier)</b> }
        </li>
      } @empty {
        <li class="gris">Aucun cours pour l'instant.</li>
      }
    </ul>
    <p>Gratuits :
      @for (c of cours(); track c.id) {
        @if (c.gratuit) { <span class="badge">{{ c.titre }}</span> }
      }
    </p>
  `,
  styles: `ul { margin: 4px 0; padding-left: 18px; } .pair { background: #f1f5f9; } b { color: #3178c6; margin-left: 6px; font-size: 12px; } .badge { background: #dcfce7; padding: 1px 8px; border-radius: 10px; margin-right: 4px; } .gris { color: #64748b; }`,
})
export class Pour {
  private suivant = 4;
  protected readonly cours = signal<Cours[]>([
    { id: 1, titre: 'TypeScript', gratuit: true },
    { id: 2, titre: 'Angular', gratuit: false },
    { id: 3, titre: 'Signals', gratuit: true },
  ]);
  protected ajouter() {
    this.cours.update((l) => [...l, { id: this.suivant, titre: `Cours ${this.suivant++}`, gratuit: false }]);
  }
  protected vider() {
    this.cours.set([]);
  }
}

Résultat de @for avec ajout de cours

Erreur NG5002 : @for loop must have a track expression

Correspondance entre *ngFor et @for

Imbriquer boucle et condition : ng-container ou blocs

  1. Bien choisir l'expression track

    • Quand la liste change, Angular compare l'ancienne et la nouvelle liste grâce à la clé donnée par track: un élément dont la clé existait déjà réutilise sa portion de DOM (et l'état qu'elle contient), un élément nouveau en crée une.
    • L'exemple suivant affiche la même liste avec trois clés différentes. Chaque ligne est un composant qui contient un champ et indique son numéro de création :
import { Component, input, signal } from '@angular/core';

interface Eleve { id: number; nom: string; }

let creees = 0;

// Une ligne qui garde un état local (le champ) : elle révèle si Angular la réutilise ou la recrée
@Component({
  selector: 'app-ligne-eleve',
  template: `<span>{{ eleve().nom }}</span> <input placeholder="appréciation" /> <small>ligne n° {{ numero }}</small>`,
  styles: `:host { display: block; padding: 2px 0; } input { width: 120px; } small { color: #94a3b8; }`,
})
export class LigneEleve {
  readonly eleve = input.required<Eleve>();
  protected readonly numero = ++creees;
}

// track : l'identifiant suit l'élève ; $index suit la position ; l'objet lui-même suit la référence
@Component({
  selector: 'app-suivi',
  imports: [LigneEleve],
  template: `
    <button (click)="inverser()">Inverser</button>
    <button (click)="recharger()">Recharger (nouveaux objets)</button>
    <div class="cols">
      <div><h5>track e.id</h5>
        @for (e of eleves(); track e.id) { <app-ligne-eleve [eleve]="e" /> }</div>
      <div><h5>track $index</h5>
        @for (e of eleves(); track $index) { <app-ligne-eleve [eleve]="e" /> }</div>
      <div><h5>track e</h5>
        @for (e of eleves(); track e) { <app-ligne-eleve [eleve]="e" /> }</div>
    </div>
  `,
  styles: `.cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } h5 { margin: 6px 0; }`,
})
export class Suivi {
  protected readonly eleves = signal<Eleve[]>([{ id: 1, nom: 'Amel' }, { id: 2, nom: 'Karim' }, { id: 3, nom: 'Sana' }]);
  protected inverser() {
    this.eleves.update((l) => [...l].reverse());
  }
  protected recharger() {
    this.eleves.update((l) => l.map((e) => ({ ...e })));
  }
}

Trois colonnes : track e.id, track $index et track e après inversion

  • Tapez une appréciation à côté d'Amel dans chaque colonne, puis cliquez sur Inverser: avec track e.id, le texte suit Amel ; avec track $index, il reste sur la première ligne, à côté de Sana.
  • Cliquez ensuite sur Recharger (mêmes élèves, nouveaux objets) : avec track e, toutes les lignes sont recréées (nouveaux numéros, saisies perdues) et la console affiche l'avertissement NG0956: The configured tracking expression (track by identity) caused re-creation of the entire collection of size 3.
  • Si deux éléments ont la même clé, Angular signale NG0955: The provided track expression resulted in duplicated keys for a given collection. Choisissez un identifiant unique et stable: id, isbn, code…

track e.id, track $index ou track e : que choisir ?

  1. [ngSwitch] et le bloc @switch

    • [ngSwitch] se place sur un conteneur et chaque cas sur un enfant (*ngSwitchCase, *ngSwitchDefault) ; il faut importer trois directives.
    • @switch (expression) contient des blocs @case (valeur) et un @default facultatif. Les valeurs sont comparées avec ===. Plusieurs @case peuvent partager le même contenu : @case ('expediee') @case ('livree') { … }.
    • Avec @default never;, le compilateur vérifie que tous les cas d'une union sont traités. Il faut pour cela tester une variable (déclarée avec @let) et non un appel de signal :
import { Component, signal } from '@angular/core';
import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';

type Statut = 'attente' | 'payee' | 'expediee' | 'livree';

// [ngSwitch] et @switch, puis un @switch exhaustif vérifié à la compilation
@Component({
  selector: 'app-choix',
  imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
  template: `
    <select (change)="statut.set($any($event.target).value)">
      @for (s of statuts; track s) { <option [value]="s" [selected]="s === statut()">{{ s }}</option> }
    </select>

    <p [ngSwitch]="statut()">
      [ngSwitch] :
      <span *ngSwitchCase="'attente'">⏳ en attente de paiement</span>
      <span *ngSwitchCase="'payee'">💳 payée</span>
      <span *ngSwitchDefault>📦 en route ou livrée</span>
    </p>

    <p>
      &#64;switch :
      @switch (statut()) {
        @case ('attente') { <span>⏳ en attente de paiement</span> }
        @case ('payee') { <span>💳 payée</span> }
        @case ('expediee') @case ('livree') { <span>📦 en route ou livrée</span> }
      }
    </p>

    @let s = statut();
    <p>
      Exhaustif :
      @switch (s) {
        @case ('attente') { <span>Étape 1/4</span> }
        @case ('payee') { <span>Étape 2/4</span> }
        @case ('expediee') { <span>Étape 3/4</span> }
        @case ('livree') { <span>Étape 4/4</span> }
        @default never;
      }
    </p>
  `,
})
export class Choix {
  protected readonly statuts: Statut[] = ['attente', 'payee', 'expediee', 'livree'];
  protected readonly statut = signal<Statut>('payee');
}

Résultat : [ngSwitch], @switch et @switch exhaustif

  • Supprimez le cas 'livree' du @switch exhaustif : la compilation échoue avec TS2322: Type '"livree"' is not assignable to type 'never'. C'est le but : ajouter un statut à l'union vous oblige à le traiter partout.
  • Écrire @switch (statut()) avec @default never; donne toujours l'erreur TS2322: Type 'Statut' is not assignable to type 'never': TypeScript ne restreint pas le résultat d'un appel de fonction.

Correspondance entre [ngSwitch] et @switch, et @default never

  1. Déclarer une variable avec @let

    • @let nom = expression; déclare une variable en lecture seule, visible dans le bloc où elle est déclarée (et ses blocs enfants), à partir de sa déclaration. Elle est recalculée à chaque affichage.
    • Elle évite de répéter un calcul ou un long chemin (commande().client.adresse.ville), et sert d'étape pour un @switch exhaustif ou un rétrécissement de type :
import { Component, signal } from '@angular/core';
import { DecimalPipe } from '@angular/common';

interface Note { matiere: string; valeur: number; coef: number; }

// @let : une variable locale au template, recalculée à chaque affichage
@Component({
  selector: 'app-variables',
  template: `
    @let total = notes().reduce((s, n) => s + n.valeur * n.coef, 0);
    @let coefs = notes().reduce((s, n) => s + n.coef, 0);
    @let moyenne = coefs ? total / coefs : 0;

    <table>
      @for (n of notes(); track n.matiere) {
        @let pondere = n.valeur * n.coef;
        <tr><td>{{ n.matiere }}</td><td>{{ n.valeur }} × {{ n.coef }}</td><td>= {{ pondere }}</td></tr>
      }
    </table>
    <p>Moyenne : <b>{{ moyenne | number: '1.2-2' }}</b> @if (moyenne >= 10) { ✅ } @else { ❌ }</p>
    <button (click)="ajouter()">Ajouter une note de 8 (coef 2)</button>
  `,
  imports: [DecimalPipe],
  styles: `td { padding: 2px 10px 2px 0; }`,
})
export class Variables {
  protected readonly notes = signal<Note[]>([
    { matiere: 'Angular', valeur: 15, coef: 3 },
    { matiere: 'TypeScript', valeur: 12, coef: 2 },
    { matiere: 'Anglais', valeur: 9, coef: 1 },
  ]);
  protected ajouter() {
    this.notes.update((l) => [...l, { matiere: `Devoir ${l.length + 1}`, valeur: 8, coef: 2 }]);
  }
}

Résultat de @let : notes pondérées et moyenne

Règles et erreurs de @let

  1. Migrer un projet vers le control flow

    • Angular fournit une commande qui convertit tous les templates d'un dossier et retire les imports devenus inutiles :
ng generate @angular/core:control-flow --path src/app
  • Voici un composant écrit avec les directives :
import { Component, signal } from '@angular/core';
import { NgFor, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';

interface Eleve { id: number; nom: string; note: number; }

@Component({
  selector: 'app-root',
  imports: [NgIf, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault],
  template: `
    <ul *ngIf="eleves().length > 0; else vide">
      <li *ngFor="let e of eleves(); let i = index; trackBy: parId">{{ i + 1 }}. {{ e.nom }}</li>
    </ul>
    <ng-template #vide><p>Aucun élève.</p></ng-template>
    <div [ngSwitch]="mention()">
      <p *ngSwitchCase="'TB'">Très bien</p>
      <p *ngSwitchCase="'B'">Bien</p>
      <p *ngSwitchDefault>Passable</p>
    </div>
  `,
})
export class App {
  protected readonly eleves = signal<Eleve[]>([{ id: 1, nom: 'Amel', note: 16 }, { id: 2, nom: 'Karim', note: 12 }]);
  protected readonly mention = signal('B');
  protected parId(_: number, e: Eleve) {
    return e.id;
  }
}
  • Et le résultat réel produit par la migration (le tableau imports est vidé, la fonction trackBy est conservée dans track) :
@Component({
  selector: 'app-root',
  imports: [],
  template: `
    @if (eleves().length > 0) {
      <ul>
        @for (e of eleves(); track parId(i, e); let i = $index) {
          <li>{{ i + 1 }}. {{ e.nom }}</li>
        }
      </ul>
    } @else {
      <p>Aucun élève.</p>
    }
    <div>
      @switch (mention()) {
        @case ('TB') {
          <p>Très bien</p>
        }
        @case ('B') {
          <p>Bien</p>
        }
        @default {
          <p>Passable</p>
        }
      }
    </div>
  `,
  • Après la migration, simplifiez track parId(i, e) en track e.id et supprimez la méthode parId.
  • Relisez toujours les fichiers modifiés et relancez les tests : la commande réécrit aussi la mise en forme du template.

Migration automatique vers @if, @for et @switch

  1. Pourquoi préférer les blocs ?

Critère *ngIf / *ngFor / [ngSwitch] @if / @for / @switch
Import NgIf, NgFor, NgSwitch… à importer Aucun : intégrés au langage
Statut Dépréciés depuis Angular 20 Syntaxe recommandée
Sinon / sinon si <ng-template #nom> séparé @else if, @else
Liste vide Un *ngIf en plus @empty
Suivi des éléments trackBy facultatif (fonction) track obligatoire (expression)
Plusieurs cas identiques Contenu dupliqué @case (a) @case (b)
Vérification des cas Aucune @default never;
Condition + boucle <ng-container> obligatoire Imbrication directe

Aide-mémoire : le contrôle de flux dans les templates Angular

  1. Activité 1 : un annuaire filtrable

    • Créer le composant Contacts: un champ de recherche (signal recherche) filtre quatre contacts (nom et ville) grâce à un computed.
    • Afficher la liste avec @for: le premier élément a une bordure haute, le dernier une bordure basse et affiche « n contact(s) » avec $count. Si rien ne correspond, @empty affiche « Aucun contact pour « … » ».

Résultat attendu de l'activité 1 : recherche « tunis »

  • Vérifier : « tunis » donne 2 contacts ; « xyz » affiche le message de la liste vide.
  • Quelle expression track avez-vous choisie, et pourquoi pas $index ici ?
  1. Activité 2 : le suivi d'une commande

    • Le type Statut vaut 'attente' | 'payee' | 'expediee' | 'livree' | 'annulee'. Une rangée de boutons (générée avec @for) change le statut ; le bouton actif est coloré.
    • Afficher un message différent par statut avec un @switch exhaustif (@let + @default never;).

Résultat attendu de l'activité 2 : colis en route

  • Vérifier : supprimer le cas 'annulee' provoque une erreur de compilation.
  • Ajoutez le statut 'retournee' à l'union : que se passe-t-il, et pourquoi est-ce une bonne chose ?
  1. Activité 3 : les états d'une connexion

    • L'état du formulaire est une union discriminée : { type: 'repos' }, { type: 'chargement' }, { type: 'erreur'; message }, { type: 'connecte'; nom }. L'envoi passe en chargement puis, après 600 ms, connecte amel@lycee.tn ou affiche une erreur.
    • Afficher chaque état avec une chaîne @if / @else if / @else sur @let e = etat();, et désactiver le bouton pendant le chargement.

Résultat attendu de l'activité 3 : erreur de connexion

  • Vérifier : {{ e.message }} n'est accepté que dans la branche e.type === 'erreur'.
  • Pourquoi faut-il @let e = etat() pour que TypeScript connaisse e.message?
  1. Activité 4 : un bulletin de notes

    • Afficher quatre notes dans un tableau : lignes paires grisées ($even), note en rouge si elle est inférieure à 10.
    • Calculer la moyenne pondérée avec @let, l'afficher avec deux décimales, puis la mention avec une chaîne @else if (Très bien ≥ 16, Bien ≥ 14, Assez bien ≥ 12, Passable ≥ 10, sinon Insuffisant).

Résultat attendu de l'activité 4 : moyenne 13,25, Assez bien

  • Vérifier : moyenne 13.25 et mention « Assez bien ».
  • Quel avantage @let a-t-il sur une méthode moyenne() appelée deux fois dans le template ?
  1. Activité 5 : migrer un catalogue

    • Réécrire ce template de catalogue avec @if, @for et @switch, d'abord à la main, puis comparer avec le résultat de ng generate @angular/core:control-flow:
<ul *ngIf="livres().length > 0; else vide">
  <li *ngFor="let l of livres(); let i = index; trackBy: parIsbn">
    {{ i + 1 }}. {{ l.titre }}
    <ng-container [ngSwitch]="l.format">
      <span *ngSwitchCase="'papier'">📕</span>
      <span *ngSwitchCase="'numerique'">💻</span>
      <span *ngSwitchDefault>🎧</span>
    </ng-container>
    <em *ngIf="l.stock === 0">épuisé</em>
  </li>
</ul>
<ng-template #vide><p>Catalogue vide.</p></ng-template>

Résultat attendu de l'activité 5 : le catalogue migré

  • Vérifier : l'affichage est identique avant et après, et le tableau imports est vide.
  • Que devient la méthode parIsbn dans votre version écrite à la main ?
  1. Activité 6 : un classement réordonnable

    • Afficher trois équipes avec @for; chaque ligne contient un champ « commentaire » et deux boutons ▲ ▼ qui échangent l'équipe avec sa voisine. ▲ est désactivé sur la première ligne ($first), ▼ sur la dernière ($last).
    • Choisir l'expression track pour que chaque commentaire reste avec son équipe quand on la déplace.

Résultat attendu de l'activité 6 : les Dauphins remontés avec leur commentaire

  • Vérifier : écrivez « favoris » à côté des Dauphins, puis faites-les monter : le commentaire les suit.
  • Remplacez track e.id par track $index: que devient le commentaire ? Expliquez.
  1. Activité 7 : tester le contrôle de flux

    • Écrire controle.spec.ts:
      • SuiviCommande affiche le bon message quand on change le signal statut;
      • Bulletin affiche « 13.25 », « Assez bien » et une seule ligne tr.faible;
      • Classement: un commentaire saisi à côté d'une équipe la suit quand on clique sur ▲.
$ ng test --include src/app/act-controle/controle.spec.ts
 ✓ Activités : contrôle de flux > SuiviCommande affiche le message du statut
 ✓ Activités : contrôle de flux > Bulletin calcule la moyenne et la mention
 ✓ Activités : contrôle de flux > Classement garde chaque commentaire avec son équipe (track e.id)

 Test Files  1 passed (1)
      Tests  3 passed (3)
  • Vérifier : 3 tests réussis.
  • Pourquoi le test du bulletin attend-il « 13.25 » et non « 13,25 » ? Que faut-il configurer pour obtenir l'écriture française ?
  • Les corrigés des sept activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier corriges/).
  1. Erreurs fréquentes

Message ou symptôme Cause Solution
NG5002: @for loop must have a "track" expression track oublié Ajouter track x.id
NG8103: The `*ngIf` directive was used in the template, but neither the `NgIf` directive nor the `CommonModule` was imported. puis NG0303: Can't bind to 'ngIf'… NgIf non importé Passer à @if (ou importer NgIf)
NG5002: Can't have multiple template bindings on one element. *ngFor et *ngIf sur le même élément <ng-container>, ou les blocs @for / @if
NG0955: The provided track expression resulted in duplicated keys for a given collection. Clé track non unique Utiliser un identifiant unique
NG0956: The configured tracking expression (track by identity) caused re-creation of the entire collection track e avec des objets recréés track e.id
Une saisie reste à la mauvaise ligne après un tri track $index sur une liste qui change d'ordre track e.id
TS2322: Type 'Statut' is not assignable to type 'never'. @default never; sur un appel de signal @let s = statut(); puis @switch (s)
TS2322: Type '"livree"' is not assignable to type 'never'. Un cas de l'union n'est pas traité Ajouter le @case manquant
NG8016: Cannot read @let declaration 'total' before it has been defined. @let lu avant sa déclaration Déplacer la déclaration plus haut
NG8015: Cannot assign to @let declaration 'total'. Affectation d'une variable @let Utiliser un signal dans la classe
TS2339: Property 'd' does not exist on type 'App'. @let utilisé hors du bloc qui le déclare Le déclarer dans le bloc parent
Un @ ou une { du texte provoque une erreur Caractère réservé des blocs &#64;, &#123;, &#125;
  1. À retenir

    • *ngIf, *ngFor et [ngSwitch] sont dépréciés depuis Angular 20 ; on les lit encore dans les anciens projets, mais on écrit @if, @for et @switch, sans import.
    • @if accepte @else if, @else et l'alias as; le contenu est retiré du DOM quand la condition est fausse.
    • @for exige track: un identifiant unique et stable (track e.id) ; $index seulement pour une liste qui ne change jamais d'ordre.
    • $index, $first, $last, $even, $odd, $count et @empty sont intégrés à @for.
    • @switch regroupe des cas (@case (a) @case (b)) et devient exhaustif avec @let et @default never;.
    • @let nomme un calcul du template, en lecture seule, visible dans son bloc.
    • ng generate @angular/core:control-flow migre un projet entier.
Un bloc @ n'est pas une directive de plus : c'est le langage du template lui-même. Plus d'import, plus de <ng-template> caché, et le compilateur vérifie davantage.