À 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.
-
Objectifs
- Lire et comprendre un template écrit avec
*ngIf,*ngForet[ngSwitch](code existant) - Écrire des conditions avec
@if,@else if,@elseet l'aliasas - Écrire des boucles avec
@for, choisir la bonne expressiontracket 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
- Lire et comprendre un template écrit avec
-
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,*ngForet[ngSwitch]. - Depuis Angular 17, le langage des templates possède ses propres blocs de contrôle (le control flow) :
@if,@foret@switch, complétés par@leten Angular 18.1. Depuis Angular 20,NgIf,NgForetNgSwitchsont 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.
- 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 :
- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/controle/. Tous les exemples ont été compilés et testés avec Angular 22.2 (composantsOnPushpar défaut, sans Zone.js).
![Évolution : des directives *ngIf, *ngFor, [ngSwitch] aux blocs @if, @for, @switch](https://apcpedagogie.com/wp-content/uploads/2026/11/angular-controle-historique.png)
-
La directive *ngIf
*ngIfaffiche 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 (NgIfdepuis@angular/common).- Le cas « sinon » passe par un
<ng-template>nommé (; else connexion), etas umémorise la valeur testée. Avecthenetelse, 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' });
}
}

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

-
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' });
}
}

- À l'intérieur du bloc, TypeScript sait que
un'est pasnull:u.nomest accepté sans?.. - Pour écrire un
@, une{ou une}comme simple texte dans un template, utilisez les entités@,{et}.

-
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 aveclet:index,first,last,even,odd,count.trackByreç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;
}
}

-
Le bloc @for
@for (c of cours(); track c.id) { … }remplace*ngFor. L'expressiontrackest 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
@emptys'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([]);
}
}




-
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 :
- Quand la liste change, Angular compare l'ancienne et la nouvelle liste grâce à la clé donnée par
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 })));
}
}

- Tapez une appréciation à côté d'Amel dans chaque colonne, puis cliquez sur Inverser: avec
track e.id, le texte suit Amel ; avectrack $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'avertissementNG0956: 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…

-
[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@defaultfacultatif. Les valeurs sont comparées avec===. Plusieurs@casepeuvent 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>
@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](https://apcpedagogie.com/wp-content/uploads/2026/11/angular-exemple-switch.png)
- Supprimez le cas
'livree'du@switchexhaustif : la compilation échoue avecTS2322: 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'erreurTS2322: 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](https://apcpedagogie.com/wp-content/uploads/2026/11/angular-ngswitch-et-switch.png)
-
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@switchexhaustif 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 }]);
}
}


-
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
importsest vidé, la fonctiontrackByest conservée danstrack) :
@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)entrack e.idet supprimez la méthodeparId. - Relisez toujours les fichiers modifiés et relancez les tests : la commande réécrit aussi la mise en forme du template.

-
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 |

-
Activité 1 : un annuaire filtrable
- Créer le composant
Contacts: un champ de recherche (signalrecherche) filtre quatre contacts (nom et ville) grâce à uncomputed. - 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,@emptyaffiche « Aucun contact pour « … » ».
- Créer le composant

- Vérifier : « tunis » donne 2 contacts ; « xyz » affiche le message de la liste vide.
- Quelle expression
trackavez-vous choisie, et pourquoi pas$indexici ?
-
Activité 2 : le suivi d'une commande
- Le type
Statutvaut'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
@switchexhaustif (@let+@default never;).
- Le type

- 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 ?
-
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, connecteamel@lycee.tnou affiche une erreur. - Afficher chaque état avec une chaîne
@if/@else if/@elsesur@let e = etat();, et désactiver le bouton pendant le chargement.
- L'état du formulaire est une union discriminée :

- Vérifier :
{{ e.message }}n'est accepté que dans la branchee.type === 'erreur'. - Pourquoi faut-il
@let e = etat()pour que TypeScript connaissee.message?
-
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).
- Afficher quatre notes dans un tableau : lignes paires grisées (

- Vérifier : moyenne 13.25 et mention « Assez bien ».
- Quel avantage
@leta-t-il sur une méthodemoyenne()appelée deux fois dans le template ?
-
Activité 5 : migrer un catalogue
- Réécrire ce template de catalogue avec
@if,@foret@switch, d'abord à la main, puis comparer avec le résultat deng generate @angular/core:control-flow:
- Réécrire ce template de catalogue avec
<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>

- Vérifier : l'affichage est identique avant et après, et le tableau
importsest vide. - Que devient la méthode
parIsbndans votre version écrite à la main ?
-
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
trackpour que chaque commentaire reste avec son équipe quand on la déplace.
- Afficher trois équipes avec

- Vérifier : écrivez « favoris » à côté des Dauphins, puis faites-les monter : le commentaire les suit.
- Remplacez
track e.idpartrack $index: que devient le commentaire ? Expliquez.
-
Activité 7 : tester le contrôle de flux
- Écrire
controle.spec.ts:SuiviCommandeaffiche le bon message quand on change le signalstatut;Bulletinaffiche « 13.25 », « Assez bien » et une seule lignetr.faible;Classement: un commentaire saisi à côté d'une équipe la suit quand on clique sur ▲.
- Écrire
$ 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/).
-
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 | @, {, } |
-
À retenir
*ngIf,*ngForet[ngSwitch]sont dépréciés depuis Angular 20 ; on les lit encore dans les anciens projets, mais on écrit@if,@foret@switch, sans import.@ifaccepte@else if,@elseet l'aliasas; le contenu est retiré du DOM quand la condition est fausse.@forexigetrack: un identifiant unique et stable (track e.id) ;$indexseulement pour une liste qui ne change jamais d'ordre.$index,$first,$last,$even,$odd,$countet@emptysont intégrés à@for.@switchregroupe des cas (@case (a) @case (b)) et devient exhaustif avec@letet@default never;.@letnomme un calcul du template, en lecture seule, visible dans son bloc.ng generate @angular/core:control-flowmigre 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.