Cours Angular · Module 2: Composants
Le flux de contrôle - la nouvelle syntaxe d'Angular 17+
Dans cette leçon6
Jusqu'ici, le template affichait tout, tout le temps et sous la même forme. Un vrai dojo ne fonctionne pas ainsi. Un guerrier sans points de vie devrait voir un message de défaite au lieu du bouton d'attaque. La liste des élèves est parfois vide, et mieux vaut alors écrire que personne n'est au dojo plutôt que montrer un cadre vide. L'insigne à côté d'un nom dépend du grade : un pour le maître, un autre pour le débutant.
Il te faut donc des conditions et des boucles dans le template lui-même. Angular 17 a introduit pour cela une syntaxe de flux de contrôle (control flow) intégrée : des blocs qui commencent par le signe @ et s'écrivent un peu comme des instructions TypeScript. Ils ont remplacé les anciennes directives structurelles *ngIf, *ngFor et *ngSwitch.
Pourquoi passer à la nouvelle syntaxe ? Les blocs se lisent comme du code ordinaire, ne demandent aucun import et rafraîchissent les listes plus vite. Les anciennes directives fonctionnent encore, mais dans un composant standalone il faut les importer (NgIf, NgFor ou tout CommonModule), et depuis Angular 20 elles sont marquées comme obsolètes. Dans du nouveau code, écris donc des blocs @.
@if - la condition à la porte
Le bloc @if n'affiche une partie du template que si une condition est remplie. Tu écris la condition entre parenthèses et le contenu entre accolades. Tu peux ensuite ajouter autant de branches @else if que tu veux et une seule branche @else :
1@if (health > 50) {
2 <p>Guerrier en pleine forme</p>
3} @else if (health > 0) {
4 <p>Guerrier blessé</p>
5} @else {
6 <p>Guerrier vaincu</p>
7}Angular vérifie les conditions de haut en bas et affiche la première branche dont la condition est vraie. Avec health égal à 30, la première condition est fausse et la deuxième vraie, donc « Guerrier blessé » apparaît. Les éléments des autres branches n'arrivent jamais sur la page : ils ne sont pas cachés, ils n'existent tout simplement pas. Quand health change, Angular bascule de branche tout seul. La branche @else est facultative, donc @if (isLoggedIn) { ... } seul est aussi un bloc valide.
Dans les projets plus anciens, tu verras le même genre de condition écrit avec une directive :
1<p *ngIf="isLoggedIn; else guestTemplate">Bienvenue !</p>
2<ng-template #guestTemplate>
3 <p>Connecte-toi</p>
4</ng-template>L'effet est le même, mais la notation est moins lisible : la branche else doit se trouver dans un élément <ng-template> séparé et nommé, et le composant doit importer NgIf. Il vaut la peine de reconnaître cette syntaxe dans le code des autres, mais dans le tien, utilise @if.
@for - la revue de l'escouade
Le bloc @for répète une partie du template pour chaque élément d'une collection. warrior of warriors signifie : pour chaque élément du tableau warriors, affiche le contenu du bloc et appelle l'élément courant warrior :
1<ul>
2 @for (warrior of warriors; track warrior.id) {
3 <li>{{ warrior.name }}</li>
4 } @empty {
5 <li>Aucun guerrier au dojo</li>
6 }
7</ul>La partie track warrior.id est obligatoire. Elle indique à Angular comment reconnaître chaque élément quand la liste change. Quand un nouveau guerrier rejoint l'escouade ou que quelqu'un s'en va, Angular compare les identifiants et ne reconstruit que les éléments qui ont vraiment changé, au lieu de redessiner toute la liste. Sans track, le template ne compile pas, et le compilateur répond par le message @for loop must have a "track" expression.
Le mieux est de suivre les éléments par un identifiant unique comme id. Quand les éléments n'ont pas d'identifiant et que la liste ne change jamais d'ordre, tu peux écrire track $index, c'est-à-dire les suivre par leur position. Le bloc @empty est facultatif : Angular l'affiche quand la collection est vide, si bien que l'élève voit un message au lieu d'une liste vide.
Les variables de contexte de la boucle
À l'intérieur de @for, Angular met à disposition plusieurs variables toutes prêtes qui décrivent le tour de boucle en cours. $index est le numéro de l'élément compté à partir de zéro, $count est le nombre total d'éléments, $first et $last indiquent si l'élément est le premier ou le dernier, et $even et $odd distinguent les positions paires et impaires. Tu peux leur donner tes propres noms avec le mot let :
1@for (warrior of warriors; track warrior.id; let i = $index, last = $last) {
2 <p [class.last]="last">{{ i + 1 }}. {{ warrior.name }}</p>
3}Ici, i contient le numéro de $index, donc la liste commence à « 1. » et non à « 0. ». La variable last ajoute la classe CSS last seulement au dernier guerrier, par exemple pour ne pas tracer de ligne de séparation sous lui. Tu peux aussi utiliser les variables de contexte sans alias, en écrivant directement {{ $index + 1 }}.
@switch - choisir un insigne
Quand une expression peut prendre une valeur parmi plusieurs valeurs connues, utilise le bloc @switch au lieu d'une chaîne de branches @else if. Chaque @case compare le résultat de l'expression avec la valeur indiquée :
1@switch (rank) {
2 @case ('master') {
3 <span class="badge">Maître</span>
4 }
5 @case ('advanced') {
6 <span class="badge">Avancé</span>
7 }
8 @default {
9 <span class="badge">Débutant</span>
10 }
11}Angular compare les valeurs avec l'opérateur ===, donc la chaîne '10' ne correspond pas au nombre 10. Seul le @case correspondant s'affiche. Contrairement à l'instruction switch de JavaScript, il n'y a pas de passage aux cas suivants, donc tu n'écris jamais de break. Le bloc @default est facultatif et n'intervient que si aucun @case ne correspond. Il peut se trouver n'importe où dans le bloc, mais par convention on l'écrit à la fin, parce que c'est ainsi qu'il se lit le plus facilement.
@let - une variable dans le template
Depuis Angular 18.1, tu peux déclarer une variable dans le template avec le bloc @let. C'est pratique quand la même expression se répète plusieurs fois ou qu'elle est assez longue pour mériter un nom :
1@let fullName = firstName + ' ' + lastName;
2@let discountedPrice = price * 0.9;
3
4<h2>{{ fullName }}</h2>
5<p>Prix après remise : {{ discountedPrice | currency }}</p>La déclaration se termine toujours par un point-virgule : @let, un nom, un signe égal, une expression et ;. La variable est en lecture seule, tu ne peux donc pas lui affecter une nouvelle valeur, mais Angular la recalcule tout seul quand firstName, lastName ou price change. Le signe | fait passer la valeur par un pipe, une fonction de mise en forme : currency transforme un nombre en montant. Un tel pipe doit être ajouté aux imports du composant sous le nom CurrencyPipe, depuis le paquet @angular/common.
L'arobase et les accolades comme simple texte
La nouvelle syntaxe a un effet secondaire. Puisque @ ouvre un bloc et que } le ferme, tu ne peux plus taper ces caractères directement dans le texte d'un template. L'adresse ango@dojo.jp dans un paragraphe se termine par une erreur de compilation, car Angular prend @dojo pour le début d'un bloc. Ces caractères s'écrivent avec des entités HTML :
1<p>Écris au maître : ango@dojo.jp</p>
2<h3>Le bloc @if</h3>
3<p>Accolade fermante : }</p>L'entité @ s'affiche comme @, { comme { et } comme }. Les doubles accolades sont un cas particulier, car Angular décode les entités avant même l'interpolation. Si tu veux afficher le texte {{ }} tel quel, ajoute à l'élément l'attribut ngNonBindable, qui désactive les liaisons dans son contenu.
La salle d'entraînement ci-dessous contient le même composant que dans la première leçon. Trouve sa section « Démo du flux de contrôle » : tu y verras @if avec des branches @else if et @else, @for avec track et @empty, et @switch avec @default. Les titres de ces sections utilisent l'entité @, et le titre de l'interpolation l'attribut ngNonBindable.
Code de cette leçon : src/app/app.component.ts
1import { Component } from '@angular/core';
2import { FormsModule } from '@angular/forms';
3
4interface Samurai {
5 id: number;
6 name: string;
7 clan: string;
8 level: number;
9 health: number;
10}
11
12@Component({
13 selector: 'app-root',
14 standalone: true,
15 imports: [FormsModule],
16 template: `
17 <div class="dojo-training">
18 <header>
19 <h1>Salle d'entraînement du Dojo</h1>
20 <p>Module 2 : les composants Angular</p>
21 </header>
22
23 <section class="training-section">
24 <h2>Démo de liaison de données</h2>
25
26 <!-- Interpolation -->
27 <div class="demo-box">
28 <h3 ngNonBindable>Interpolation {{ }}</h3>
29 <p>Nom : {{ samurai.name }}</p>
30 <p>Clan : {{ samurai.clan }}</p>
31 </div>
32
33 <!-- Liaison de propriété -->
34 <div class="demo-box">
35 <h3>Liaison de propriété [ ]</h3>
36 <progress [value]="samurai.health" max="100"></progress>
37 <p>Santé : {{ samurai.health }} %</p>
38 </div>
39
40 <!-- Liaison d'événement -->
41 <div class="demo-box">
42 <h3>Liaison d'événement ( )</h3>
43 <button (click)="attack()">Attaquer !</button>
44 <button (click)="heal()">Soigner</button>
45 </div>
46
47 <!-- Liaison bidirectionnelle -->
48 <div class="demo-box">
49 <h3>Liaison bidirectionnelle [( )]</h3>
50 <input [(ngModel)]="samurai.name" placeholder="Nom du samouraï">
51 <p>Nom actuel : {{ samurai.name }}</p>
52 </div>
53 </section>
54
55 <section class="control-flow-section">
56 <h2>Démo du flux de contrôle</h2>
57
58 <!-- @if -->
59 <div class="demo-box">
60 <h3>@if / @else</h3>
61 @if (samurai.health > 50) {
62 <p class="status good">Samouraï en pleine forme !</p>
63 } @else if (samurai.health > 20) {
64 <p class="status warning">Samouraï affaibli</p>
65 } @else {
66 <p class="status danger">Samouraï dans un état critique !</p>
67 }
68 </div>
69
70 <!-- @for -->
71 <div class="demo-box">
72 <h3>@for</h3>
73 <ul>
74 @for (s of samuraiList; track s.id) {
75 <li [class.selected]="s.id === selectedId" (click)="selectSamurai(s.id)">
76 {{ s.name }} (niv. {{ s.level }})
77 </li>
78 } @empty {
79 <li>Aucun samouraï</li>
80 }
81 </ul>
82 </div>
83
84 <!-- @switch -->
85 <div class="demo-box">
86 <h3>@switch</h3>
87 @switch (getRank(samurai.level)) {
88 @case ('master') {
89 <span class="badge master">Maître</span>
90 }
91 @case ('advanced') {
92 <span class="badge advanced">Avancé</span>
93 }
94 @default {
95 <span class="badge beginner">Débutant</span>
96 }
97 }
98 </div>
99 </section>
100 </div>
101 `,
102 styles: [`
103 .dojo-training {
104 font-family: 'Segoe UI', sans-serif;
105 max-width: 900px;
106 margin: 0 auto;
107 padding: 20px;
108 background: #1a1a2e;
109 color: #eee;
110 }
111
112 header {
113 text-align: center;
114 border-bottom: 2px solid #c9a227;
115 padding-bottom: 20px;
116 margin-bottom: 20px;
117 }
118
119 h1 { color: #c9a227; }
120 h2 { color: #c9a227; margin-top: 30px; }
121 h3 { color: #888; font-size: 0.9rem; margin-bottom: 10px; }
122
123 .demo-box {
124 background: rgba(255,255,255,0.05);
125 border: 1px solid #333;
126 border-radius: 8px;
127 padding: 15px;
128 margin-bottom: 15px;
129 }
130
131 button {
132 background: #c9a227;
133 color: #1a1a2e;
134 border: none;
135 padding: 10px 20px;
136 margin: 5px;
137 border-radius: 4px;
138 cursor: pointer;
139 }
140
141 button:hover { opacity: 0.9; }
142
143 input {
144 padding: 10px;
145 border: 1px solid #c9a227;
146 border-radius: 4px;
147 background: #16213e;
148 color: #eee;
149 width: 200px;
150 }
151
152 progress {
153 width: 100%;
154 height: 20px;
155 }
156
157 .status { padding: 10px; border-radius: 4px; }
158 .status.good { background: #2d6a4f; }
159 .status.warning { background: #b45309; }
160 .status.danger { background: #991b1b; }
161
162 ul { list-style: none; padding: 0; }
163 li {
164 padding: 10px;
165 margin: 5px 0;
166 background: #16213e;
167 border-radius: 4px;
168 cursor: pointer;
169 }
170 li:hover { background: #1e3a5f; }
171 li.selected { border: 2px solid #c9a227; }
172
173 .badge {
174 padding: 5px 15px;
175 border-radius: 20px;
176 font-weight: bold;
177 }
178 .badge.master { background: #c9a227; color: #1a1a2e; }
179 .badge.advanced { background: #7c3aed; }
180 .badge.beginner { background: #4a5568; }
181 `]
182})
183export class AppComponent {
184 samurai: Samurai = {
185 id: 1,
186 name: 'Musashi',
187 clan: 'Niten Ichi-ryū',
188 level: 8,
189 health: 100
190 };
191
192 samuraiList: Samurai[] = [
193 { id: 1, name: 'Musashi', clan: 'Niten', level: 10, health: 100 },
194 { id: 2, name: 'Kojiro', clan: 'Ganryū', level: 9, health: 85 },
195 { id: 3, name: 'Jubei', clan: 'Yagyu', level: 7, health: 90 }
196 ];
197
198 selectedId = 1;
199
200 attack(): void {
201 this.samurai.health = Math.max(0, this.samurai.health - 15);
202 }
203
204 heal(): void {
205 this.samurai.health = Math.min(100, this.samurai.health + 20);
206 }
207
208 selectSamurai(id: number): void {
209 this.selectedId = id;
210 }
211
212 getRank(level: number): string {
213 if (level >= 9) return 'master';
214 if (level >= 5) return 'advanced';
215 return 'beginner';
216 }
217}Retiens bien : @if garde la porte, @for passe l'escouade en revue et a toujours besoin de track, @switch distribue les insignes, et @let donne un nom à ce que tu répètes.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle est la nouvelle syntaxe d'affichage conditionnel dans Angular 17+ ?
2. Qu'est-ce qui est requis dans le nouveau bloc @for pour qu'Angular puisse suivre de manière optimale les éléments de la liste ?
3. Quel bloc ajoute-t-on à @for pour afficher du contenu lorsque la collection est vide ?
Tâches pratiques dans le jeu
- Clique dans l'ordre
Ordonne les éléments pour ouvrir un bloc conditionnel : @if (isLoggedIn) {
- Éditeur de code
Complète le template en utilisant @if pour l'affichage conditionnel et @for pour itérer sur les guerriers.
- Tri vertical
Ordonne les lignes du bloc @switch dans le bon ordre (de haut en bas) :
- Tri horizontal
Ordonne les éléments sur une seule ligne pour déclarer une variable dans le template : @let fullName = firstName;