Cours Angular · Module 2: Composants

Le flux de contrôle - la nouvelle syntaxe d'Angular 17+

7 min de lecture
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&#64;dojo.jp</p>
2<h3>Le bloc &#64;if</h3>
3<p>Accolade fermante : &#125;</p>

L'entité &#64; s'affiche comme @, &#123; comme { et &#125; 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é &#64;, 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>&#64;if / &#64;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>&#64;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>&#64;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. 1. Quelle est la nouvelle syntaxe d'affichage conditionnel dans Angular 17+ ?

  2. 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. 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;

Articles utiles