Cours Angular · Module 2: Composants
La liaison de données - comment la classe parle au template
Dans cette leçon5
Quand le samouraï encaisse un coup et que sa santé passe de 100 à 70, l'écran doit afficher 70 et non l'ancienne valeur. Quand l'élève clique sur le bouton d'attaque, la classe doit l'apprendre pour retirer des points de vie à l'adversaire.
Sans l'aide du framework, tu devrais tout faire à la main : chercher les éléments de la page, réécrire leur texte après chaque changement de données et brancher des écouteurs de clic. Chaque étape oubliée donne un écran qui ment. Angular le fait pour toi grâce à la liaison de données (data binding). Dans le template, tu décris une seule fois ce qui est relié à quoi, et Angular veille à ce que la classe et la vue disent toujours la même chose.
Sur les champs de bataille d'autrefois, le commandant communiquait avec ses troupes par des bannières et des tambours. La bannière portait l'ordre du commandant aux soldats, et le tambour renvoyait le rapport dans l'autre sens. La liaison de données fonctionne de manière semblable et existe sous quatre formes :
| Syntaxe | Direction | Exemple |
|---|---|---|
{{ expression }} | classe → template (texte) | {{ name }} |
[propriété]="expression" | classe → template (propriété de l'élément) | [disabled]="isDefeated" |
(événement)="instruction" | template → classe | (click)="attack()" |
[(ngModel)]="champ" | dans les deux sens | [(ngModel)]="samuraiName" |
Voyons-les une par une, en commençant par la plus simple.
L'interpolation - le message sur la bannière
L'interpolation insère une valeur de la classe dans le texte de la page. Tu écris l'expression entre doubles accolades, et Angular l'évalue puis affiche le résultat à cet endroit :
1<h2>{{ name }}</h2>
2<p>Niveau : {{ level }}</p>
3<p>Force du coup : {{ level * 10 }}</p>
4<p>{{ name.toUpperCase() }}</p>Les accolades peuvent contenir un nom de propriété, un calcul ou un appel de méthode. Il doit cependant s'agir d'une expression, c'est-à-dire de quelque chose qui renvoie une valeur, et non d'une instruction. {{ level = 5 }} ne compile pas, car Angular interdit les affectations dans l'interpolation. Les accolades servent à montrer les données, pas à les modifier.
Le résultat de l'interpolation arrive toujours sur la page sous forme de texte. Si le champ name contient une balise <b>, l'élève la voit telle quelle au lieu d'un texte en gras. C'est une protection voulue : des données saisies par un utilisateur ne peuvent pas injecter ainsi leur propre HTML dans la page. Quand la valeur change dans la classe, Angular remplace le texte à l'écran tout seul.
La liaison de propriété - fixer des propriétés
L'interpolation insère du texte, mais il faut parfois fixer une propriété d'un élément : l'adresse d'une image, l'état d'un bouton ou une classe CSS. C'est le rôle de la liaison de propriété (property binding) : le nom de la propriété entre crochets et une expression après le signe égal :
1<img [src]="bannerUrl" [alt]="name">
2<button [disabled]="isDefeated">Attaquer !</button>
3<li [class.active]="isActive">Musashi</li>Les crochets disent à Angular : évalue ce qui se trouve entre les guillemets comme une expression. [src]="bannerUrl" fixe la propriété src de l'image à la valeur du champ bannerUrl de la classe. [disabled]="isDefeated" désactive le bouton tant que isDefeated vaut true. La forme spéciale [class.active] ajoute la classe CSS active quand l'expression est vraie et la retire quand elle est fausse.
Sans crochets, la partie droite est une simple chaîne. C'est là qu'un piège attend beaucoup de débutants, alors compare ces deux boutons :
1<!-- bouton TOUJOURS désactivé : l'attribut disabled existe -->
2<button disabled="false">Attaquer !</button>
3
4<!-- bouton activé : la propriété reçoit la valeur false -->
5<button [disabled]="false">Attaquer !</button>Dans la première ligne, false n'est que le texte de l'attribut, et pour le navigateur seule la présence de l'attribut disabled compte, donc le bouton reste désactivé. Dans la seconde, Angular évalue l'expression false et donne à la propriété de l'élément une vraie valeur booléenne, donc le bouton fonctionne. C'est pourquoi tout ce qui doit changer avec les données se lie entre crochets.
Pour du texte, les crochets ne sont pas nécessaires. [title]="'Musashi'" et title="Musashi" produisent exactement le même effet, car dans le premier cas l'expression est simplement une chaîne entre apostrophes. Les crochets deviennent indispensables quand la valeur vient de la classe ou n'est pas du texte, par exemple avec le nombre [level]="10", que tu transmettras à un composant enfant dans la dernière leçon du module.
La liaison d'événement - un rapport du champ de bataille
La liaison de propriété transmet des données de la classe au template. La liaison d'événement (event binding) fonctionne dans l'autre sens : quand quelque chose se produit sur la page, comme un clic, Angular appelle la méthode de la classe que tu as indiquée. Tu écris le nom de l'événement entre parenthèses :
1<button (click)="attack()">Attaquer !</button>
2<input (input)="onNameInput($event)">La première ligne appelle la méthode attack() à chaque clic. La seconde réagit à chaque modification du texte dans le champ et passe à la méthode la variable spéciale $event. Pour les événements du navigateur, $event est l'objet de l'événement, dans lequel tu peux lire par exemple l'élément où l'événement s'est produit :
1export class SamuraiComponent {
2 name = 'Musashi';
3
4 onNameInput(event: Event): void {
5 const field = event.target as HTMLInputElement;
6 this.name = field.value;
7 }
8}La propriété event.target désigne le champ de texte, et as HTMLInputElement indique à TypeScript qu'il s'agit bien d'un élément input, dont on peut donc lire la value. Une fois le nouveau nom affecté, Angular rafraîchit tous les endroits du template qui affichent name.
Méfie-toi de trois notations qui se ressemblent mais ne sont pas des liaisons d'événement Angular. [click]="attack()" est une liaison de propriété : Angular traite click comme une propriété de l'élément et non comme un événement. onclick="attack()" est un simple attribut HTML qui ne voit pas les méthodes de ta classe. @click="attack()" est la syntaxe de Vue, qu'Angular ne comprend pas. Dans Angular, un événement s'écrit toujours entre parenthèses.
La liaison bidirectionnelle - une conversation dans les deux sens
Un champ de formulaire a besoin des deux directions à la fois. La classe doit afficher le nom actuel du samouraï dans le champ, et quand l'élève en tape un nouveau, la classe doit le connaître aussitôt. C'est ce que relie la liaison bidirectionnelle (two-way binding), qui s'écrit avec des parenthèses à l'intérieur de crochets : [(ngModel)]. La directive ngModel vient de FormsModule, donc le composant doit l'avoir dans sa liste imports :
1import { Component } from '@angular/core';
2import { FormsModule } from '@angular/forms';
3
4@Component({
5 selector: 'app-samurai-form',
6 standalone: true,
7 imports: [FormsModule],
8 template: `
9 <input [(ngModel)]="samuraiName">
10 <p>Nom du samouraï : {{ samuraiName }}</p>
11 `
12})
13export class SamuraiFormComponent {
14 samuraiName = '';
15}Chaque lettre tapée dans le champ va directement dans la propriété samuraiName, et l'interpolation du paragraphe affiche la nouvelle valeur. Cela marche aussi dans l'autre sens : si une méthode de la classe affecte un nouveau nom à samuraiName, le champ de texte l'affiche tout seul. Sans FormsModule dans la liste imports, le compilateur signale qu'il ne peut pas lier ngModel, parce que l'élément input n'a pas de telle propriété.
Les développeurs appellent cette notation la banane dans la boîte : la banane, ce sont les parenthèses, posée dans la boîte, les crochets. C'est un raccourci qu'Angular développe en deux liaisons que tu connais déjà :
1<input [ngModel]="samuraiName" (ngModelChange)="samuraiName = $event">La liaison de propriété [ngModel] écrit la valeur de la classe dans le champ, et la liaison d'événement (ngModelChange) réaffecte la nouvelle valeur de $event à la classe à chaque modification. Les deux notations fonctionnent à l'identique, mais le raccourci se lit plus vite et laisse moins de place aux erreurs. L'ordre des signes compte : la forme inversée ([ngModel]) n'est pas une liaison bidirectionnelle, et le compilateur d'Angular avertit que les parenthèses doivent se trouver à l'intérieur.
Exemple complet - le panneau d'entraînement
Réunissons les quatre liaisons dans un seul composant. Le panneau d'entraînement permet de saisir un nombre de répétitions, d'effectuer un entraînement et de suivre le total de toutes les répétitions :
1import { Component } from '@angular/core';
2import { FormsModule } from '@angular/forms';
3
4@Component({
5 selector: 'app-training',
6 standalone: true,
7 imports: [FormsModule],
8 template: `
9 <div class="training-panel">
10 <h2>{{ trainingName }}</h2>
11
12 <input [(ngModel)]="reps" type="number">
13 <button
14 [disabled]="reps <= 0"
15 (click)="doTraining()">
16 S'entraîner !
17 </button>
18
19 <p>Répétitions effectuées : {{ totalReps }}</p>
20 </div>
21 `
22})
23export class TrainingComponent {
24 trainingName = 'Entraînement au sabre';
25 reps = 10;
26 totalReps = 0;
27
28 doTraining(): void {
29 this.totalReps += this.reps;
30 }
31}Suis ce code pas à pas. L'interpolation {{ trainingName }} affiche le nom de l'entraînement dans le titre. La liaison bidirectionnelle [(ngModel)]="reps" relie le champ numérique à la propriété reps, donc chaque nombre saisi arrive directement dans la classe. La liaison de propriété [disabled]="reps <= 0" désactive le bouton quand le nombre de répétitions est nul ou négatif. La liaison d'événement (click)="doTraining()" appelle la méthode qui ajoute reps à totalReps, et l'interpolation du dernier paragraphe affiche le nouveau total.
Remarque ce que cette classe ne fait pas : elle ne cherche aucun élément de la page et ne réécrit pas le texte du paragraphe. Elle ne modifie que ses propres propriétés ordinaires, et les liaisons s'occupent du reste. Mon conseil : garde la logique dans les méthodes de la classe et contente-toi de l'appeler depuis le template, car de longs calculs écrits entre les guillemets du template deviennent vite illisibles.
Dans la dernière leçon de ce module, tu transmettras des données à un composant enfant par liaison de propriété, et dans le sixième module, tu construiras ce même panneau avec des signaux.
La salle d'entraînement ci-dessous contient le même composant que dans la première leçon. Trouve sa section « Démo de liaison de données » : l'interpolation du nom et du clan du samouraï, la liaison de propriété [value] de la barre de santé, la liaison d'événement des boutons Attaquer ! et Soigner, et la liaison bidirectionnelle du champ du nom.
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 : les accolades montrent les données, les crochets fixent les propriétés, les parenthèses apportent les rapports d'événements, et la banane dans la boîte mène une conversation dans les deux sens.
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 syntaxe est utilisée pour l'interpolation dans un template Angular ?
2. Quelle syntaxe est utilisée pour écouter les événements dans Angular ?
3. Quelle syntaxe est utilisée pour la liaison de propriété dans Angular ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne les éléments sur une seule ligne pour créer une liaison bidirectionnelle entre la valeur d'un champ input et la propriété samuraiName :
- Éditeur de code
Complète le code : ajoute une liaison de propriété pour [disabled] et une liaison d'événement pour (click) sur le bouton d'attaque.
- Clique dans l'ordre
Ordonne les caractères pour construire une liaison de propriété pour l'attribut src : [src]
- Tri horizontal
Ordonne les éléments sur une seule ligne pour lier la méthode onAttack() au clic d'un bouton :