Un template n’est pas du HTML statique : Angular le garde relié à la classe du composant. Les crochets et parenthèses indiquent le sens des données — {{ }} et [ ] portent des valeurs de la classe vers le DOM, ( ) ramène des événements dans la classe, et [( )] fait les deux.
La plupart des bugs de débutant ici ne sont pas des erreurs de syntaxe. Ils viennent de trois questions : dans quel sens vont les données, est-ce que je modifie une propriété du DOM ou un attribut HTML, et quel élément a vraiment reçu le clic ?
Quatre bindings, deux sens
Les données descendent avec {{ }} et [ ], les événements remontent avec ( ). Angular réévalue les expressions de droite quand les signals qu’elles lisent changent, et n’écrit dans le DOM que si le résultat diffère de ce qu’il a écrit la dernière fois.
| Syntaxe | Sens | Ce qui se passe |
|---|---|---|
{{ expr }} | classe → DOM | définit le texte d’un nœud (toujours du texte, jamais du HTML) |
[prop]="expr" | classe → DOM | définit une propriété de l’élément DOM |
[attr.x]="expr" | classe → DOM | ajoute ou retire un attribut HTML |
(event)="stmt" | DOM → classe | exécute une instruction quand l’événement survient |
[(x)]="sig" | dans les deux sens | un property binding et un événement de changement, en un seul |
<!-- classe → DOM -->
<h2>{{ title() }}</h2> <!-- interpolation : du texte -->
<img [src]="avatarUrl()" [alt]="userName()" /> <!-- property binding -->
<td [attr.colspan]="wide() ? 2 : null"></td> <!-- attribute binding -->
<!-- DOM → classe -->
<button (click)="save()">Enregistrer</button> <!-- event binding -->
<!-- dans les deux sens -->
<app-rating [(value)]="stars" /> <!-- two-way binding -->L’interpolation : toujours du texte
{{ }} transforme la valeur en chaîne et la place dans un nœud texte. Le HTML contenu dans la valeur s’affiche tel quel, il n’est pas interprété — c’est ce qui vous protège des XSS. On lit un signal en l’appelant : {{ count() }}. Gardez l’expression légère et sans effet de bord : elle peut s’exécuter à chaque passage de la détection de changements.
<!-- la valeur devient du texte : '<b>Ana</b>' s’affiche avec ses chevrons, pas en gras -->
<p>Bonjour, {{ userName() }}!</p>
<!-- n’importe quelle expression sans effet de bord ; on appelle les signals -->
<p>{{ items().length }} articles · {{ total() | currency: 'EUR' }}</p>
<p>{{ user()?.address?.city ?? 'pas encore de ville' }}</p>
<!-- ✗ le texte de la fonction du signal, pas sa valeur -->
<p>{{ count }}</p>Le property binding définit une propriété du DOM
[value]="name()" ne touche pas au HTML. Il définit la propriété value de l’élément input. Les attributs sont les valeurs de départ écrites dans le HTML ; les propriétés sont l’état vivant de l’élément. Quand l’utilisateur tape, la propriété change et l’attribut reste tel quel. Et Angular n’écrit la propriété que lorsque name() change — si la classe contient toujours la même valeur, une réinitialisation ne change pas ce que l’utilisateur a tapé.
@Component({
selector: 'app-profile',
template: `
<!-- définit input.value ; ne change pas l’attribut value="" -->
<input [value]="name()" (input)="name.set(box.value)" #box />
<button [disabled]="saving()" (click)="save()">Enregistrer</button>
<button (click)="reset()">Réinitialiser</button>
`,
})
export class Profile {
readonly name = signal('Ana');
readonly saving = signal(false);
reset() {
// ne marche que parce que (input) garde name() à jour ;
// avec [value] seul, name() vaut toujours 'Ana' et set('Ana') ne change rien
this.name.set('Ana');
}
}name()dans la classe | 'Ana' |
input.valuepropriété DOM | 'Ana' |
getAttribute('value')attribut HTML | null |
synchroniséLe champ et le signal disent tous deux 'Ana'.
Le premier bouton est désactivé : l’attribut existe, et sa valeur texte ne compte pas.
Dans le premier mode, le champ affiche un texte dont la classe ne sait rien. name.set('Ana') donne au signal la valeur qu’il avait déjà : rien ne change, et Angular n’écrit pas dans l’input. Relire l’input avec (input) — ou lier dans les deux sens avec [( )] ou un formulaire — garde la classe et le DOM au même pas. Remarquez que getAttribute('value') reste null du début à la fin : Angular n’a jamais défini l’attribut.
Quand il n’y a pas de propriété : [attr.]
Certains attributs n’ont pas de propriété correspondante : aria-*, colspan, data-*, les attributs SVG. Pour eux, utilisez [attr.nom]. Une valeur null ou undefined retire l’attribut — c’est ainsi qu’on active et désactive aria-current. Les classes et les styles ont leurs propres bindings, présentés dans [class] et [style].
Les attributs booléens sont le piège classique : le navigateur regarde seulement si l’attribut existe. disabled="false" est une chaîne, l’attribut est présent, et le bouton est désactivé. [disabled]="false" donne à la propriété le booléen false.
<!-- pas de propriété DOM pour ceux-là : on lie l’attribut -->
<td [attr.colspan]="span()">…</td>
<svg><circle [attr.r]="radius()" /></svg>
<div [attr.data-state]="state()"></div>
<!-- null retire l’attribut : aria-current n’existe que sur le lien actif -->
<a [attr.aria-current]="active() ? 'page' : null" href="/orders">Commandes</a>
<!-- ✗ l’attribut existe, donc le bouton est désactivé -->
<button disabled="false">Enregistrer</button>
<!-- ✓ la propriété reçoit le booléen false -->
<button [disabled]="false">Enregistrer</button>L’event binding : ( )
(click)="save()" ajoute un listener sur l’élément et exécute l’instruction quand l’événement survient. $event est l’objet de l’événement DOM : un MouseEvent pour un clic, un KeyboardEvent pour une touche, ou la valeur émise par l’output() d’un composant. Pour lire un champ, une référence de template comme #box est plus simple et entièrement typée : (input)="name.set(box.value)".
<button (click)="save()">Enregistrer</button>
<!-- $event est l’événement DOM ; pour un output(), c’est la valeur émise -->
<div (pointermove)="track($event.clientX, $event.clientY)"></div>
<app-rating (valueChange)="stars.set($event)" />
<!-- une référence de template est typée HTMLInputElement : pas de cast -->
<input #box (input)="query.set(box.value)" />
<!-- ✗ en mode strict : 'value' does not exist on type 'EventTarget' -->
<input (input)="query.set($event.target.value)" />Les événements remontent (bubbling)
Un clic part de l’élément le plus profond puis remonte par chaque ancêtre, en exécutant aussi leurs handlers (click). C’est ainsi qu’un bouton Supprimer placé dans une ligne cliquable ouvre aussi la ligne — le handler de la ligne s’exécute juste après celui du bouton.
- Commande n°1042
- Commande n°1043
- Commande n°1044
Aucun clic pour l’instant.
$event.target est l’élément où le clic a commencé, et il est le même dans tous les handlers. $event.currentTarget est l’élément dont le handler s’exécute. stopPropagation() dans le handler intérieur est la solution rapide. Une meilleure structure consiste souvent à ne rien imbriquer : l’action de la ligne devient un vrai lien ou un vrai bouton à côté de Supprimer, pas un handler de clic sur toute la ligne — et les utilisateurs du clavier peuvent alors l’ouvrir aussi.
<ul>
@for (order of orders(); track order.id) {
<!-- la ligne ouvre la commande… -->
<li (click)="open(order)">
Commande n°{{ order.id }}
<!-- …et sans stopPropagation(), un clic ici atteint aussi la ligne -->
<button (click)="remove(order); $event.stopPropagation()">Supprimer</button>
</li>
}
</ul>
<!-- souvent mieux : deux actions séparées, sans imbrication, accessibles au clavier -->
<li>
<a [routerLink]="['/orders', order.id]">Commande n°{{ order.id }}</a>
<button (click)="remove(order)">Supprimer</button>
</li>Filtres de touches et action par défaut du navigateur
Angular permet de filtrer les événements clavier directement dans le binding : (keydown.enter), (keydown.escape), (keydown.shift.enter), (keydown.control.s). Les modificateurs doivent correspondre exactement : (keydown.enter) ne se déclenche pas pour Maj+Entrée. Votre handler s’exécute avant l’action par défaut du navigateur — insérer un saut de ligne, envoyer le formulaire, suivre un lien — et $event.preventDefault() annule cette action.
<textarea
#msg
[value]="draft()"
(input)="draft.set(msg.value)"
(keydown.enter)="send($event)"
(keydown.escape)="draft.set('')"
></textarea>
<!-- (keydown.enter) ne se déclenche pas pour Maj+Entrée : le navigateur ajoute une ligne, comme d’habitude -->(keydown) | — |
(keydown.enter) | — |
(keydown.shift.enter) | — |
(keydown.escape) | — |
Appuyez sur une touche dans la zone de message.
Sans preventDefault(), le navigateur insère le saut de ligne après que votre handler a vidé draft, et l’événement (input) réécrit le texte. Un détail de plus que la plupart des zones de chat ratent : pendant qu’on tape du japonais ou du chinois avec une méthode de saisie (IME), Entrée valide les caractères. Vérifiez $event.isComposing et ignorez la touche dans ce cas.
[( )], c’est [ ] plus ( )
La « banane dans la boîte » est un raccourci. [(value)]="name" signifie un property binding sur value plus un listener pour l’événement valueChange qui réécrit la valeur. Si vous passez un WritableSignal, Angular met le signal à jour pour vous. Dans vos propres composants, model() crée les deux moitiés ; pour les champs de formulaire, utilisez Signal Forms.
<!-- le raccourci… -->
<app-rating [(value)]="stars" />
<!-- …signifie exactement ceci -->
<app-rating [value]="stars()" (valueChange)="stars.set($event)" />
<!-- avec un WritableSignal, Angular appelle set() pour vous ;
l’enfant le déclare avec : value = model(0) -->Les variables de référence de template
#nom sur un élément vous donne cet élément dans le reste du template ; sur un composant, l’instance du composant. C’est le moyen le plus rapide de lire un champ ou d’appeler focus() sans passer par $event.target, dont le type n’est que EventTarget. Dans la classe, la même référence s’obtient avec viewChild().
<!-- sur un élément : l’élément lui-même -->
<input #search placeholder="Rechercher" />
<button (click)="search.focus()">Focus</button>
<!-- sur un composant : l’instance du composant -->
<app-video-player #player />
<button (click)="player.play()">Lecture</button>
<!-- dans la classe : readonly search = viewChild<ElementRef<HTMLInputElement>>('search') -->Bonnes habitudes / à éviter
- Garder la source de vérité dans la classe (un signal) et relire chaque changement depuis le DOM.
- Utiliser des références de template (#box) plutôt que $event.target quand il vous faut une valeur.
- Utiliser [attr.] pour aria-* et data-*, et null pour retirer un attribut.
- Appeler des fonctions coûteuses dans {{ }} ou [ ] — elles se réexécutent à chaque passage.
- Les attributs booléens statiques avec une valeur (disabled="false") : seule la présence de l’attribut compte.
- stopPropagation() par habitude : l’événement disparaît aussi pour l’analytics, pour les menus qui se ferment au clic à l’extérieur, et pour vos collègues.
Pièges fréquents
- Une réinitialisation qui ne réinitialise rien. Le champ était lié dans un seul sens, l’utilisateur a tapé, et la classe garde l’ancienne valeur. La redéfinir ne change rien.
- Oublier les parenthèses.
{{ count }}affiche la fonction du signal, pas sa valeur. Le compilateur vous prévient ; n’ignorez pas l’avertissement. - La ligne s’ouvre quand on clique sur Supprimer. Le clic est remonté du bouton à la ligne. Arrêtez-le, ou n’imbriquez pas les actions.
- Un saut de ligne reste après l’envoi. L’action par défaut d’Entrée s’est exécutée après le handler. Appelez
$event.preventDefault(). - Écrire
$event.target.value.targetest unEventTarget, donc cela ne compile pas en mode strict. Utilisez une référence de template, ou lisez la valeur dans la classe avec un cast.
Aide-mémoire
{{ expr }} du texte dans le DOM ; le HTML est échappé ; on appelle les signals : {{ count() }}
[prop]="expr" une propriété DOM (value, disabled, src, checked) ou un input de composant
[attr.x]="expr" un attribut HTML (aria-*, data-*, colspan, svg) ; null le retire
[class.x] / [style.x] une classe ou un style ; voir la page [class] et [style]
(event)="stmt" un listener ; $event = l’événement DOM ou la valeur émise
(keydown.enter) filtres de touches ; les modificateurs doivent correspondre : shift.enter, control.s, escape
[(x)]="sig" [x]="sig()" + (xChange)="sig.set($event)"
#ref l’élément, ou l’instance du composant, dans le template
quand Angular écrit seulement si la valeur diffère de la dernière écrite
attribut ou propriété attribut = valeur de départ dans le HTML ; propriété = état vivant
bubbling un clic exécute les handlers de chaque ancêtre ; target reste le même
actions par défaut s’exécutent après votre handler ; $event.preventDefault() les annule