À propos de ce cours
La leçon approfondit ce que « La syntaxe de base d’Angular » avait seulement présenté. Tous les exemples compilent avec Angular 22 et je les ai testés dans le navigateur ; chaque résultat ou message d’erreur affiché dans la leçon a été réellement obtenu. Il y a 8 sections :
Quand Angular met-il l’affichage à jour ?
L’interpolation en détail : ?.
Propriété du DOM ou attribut HTML.
Classes et stylesLiaison bidirectionnelle : [( )] décomposé en [ ] + ( ), ngModel, [ngValue], updateOn: 'blur' et model().
Signal Forms d’Angular 22 : form(), [formField] et la validation.
Aide-mémoire.
-
Objectifs
- Comprendre ce qu'est le data binding et ses quatre formes: interpolation, liaison de propriété, liaison d'événement et liaison bidirectionnelle
- Savoir quand Angular met l'affichage à jour, et pourquoi les signals sont la bonne manière de stocker les données affichées
- Distinguer une propriété du DOM d'un attribut HTML, et lier des classes CSS et des styles
- Exploiter
$event, les filtres de touches,preventDefault()etstopPropagation() - Maîtriser la liaison bidirectionnelle avec
ngModel,model()et les Signal Forms d'Angular 22
-
Présentation
- Dans un composant, la classe contient les données et le template les affiche. Le data binding (liaison de données) est le mécanisme qui les relie : quand une donnée change, l'affichage suit ; quand l'utilisateur agit, la classe en est informée.
- Vous avez découvert ses quatre syntaxes dans le chapitre « La syntaxe de base d'Angular ». Ce chapitre les approfondit, avec leurs subtilités, leurs pièges et les nouveautés des versions récentes.
- les trois premières liaisons sont à sens unique: de la classe vers le template (
{{ }},[ ]) ou du template vers la classe (( )) ; - la liaison bidirectionnelle
[( )]combine une liaison de propriété et une liaison d'événement.

- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/binding/. Tous les exemples ont été testés avec Angular 22.
-
Quand Angular met-il l'affichage à jour ?
- Angular ne surveille pas vos variables en permanence. Il revérifie un composant lorsqu'il a une raison de le faire : un signal lu par le template a changé, un événement lié dans le template s'est produit, ou une entrée du composant a reçu une nouvelle valeur.
- Depuis Angular 21, les nouveaux projets n'utilisent plus la bibliothèque
zone.js, qui détectait autrefois tous les changements, et Angular 22 applique la stratégieOnPushpar défaut. Une propriété ordinaire modifiée dans un minuteur n'est donc plus affichée automatiquement.

import { Component, DestroyRef, inject, signal } from '@angular/core';
@Component({
selector: 'app-horloge-propriete',
template: `<p>Propriété ordinaire : {{ secondes }} s <button type="button" (click)="rafraichir()">Rafraîchir</button></p>`,
})
export class HorlogePropriete {
protected secondes = 0;
constructor() {
const id = setInterval(() => this.secondes++, 1000);
inject(DestroyRef).onDestroy(() => clearInterval(id));
}
protected rafraichir(): void {}
}
@Component({
selector: 'app-horloge-signal',
template: `<p>Signal : {{ secondes() }} s</p>`,
})
export class HorlogeSignal {
protected readonly secondes = signal(0);
constructor() {
const id = setInterval(() => this.secondes.update((s) => s + 1), 1000);
inject(DestroyRef).onDestroy(() => clearInterval(id));
}
}
- Après trois secondes, la page affiche
Signal : 3 smais toujoursPropriété ordinaire : 0 s. Un clic sur Rafraîchir (un événement lié, même vers une méthode vide) provoque une vérification : la propriété affiche alors brusquement3 s, puis se fige de nouveau. inject(DestroyRef).onDestroy()arrête le minuteur quand le composant disparaît : sans cela, il continuerait à tourner en mémoire.- Règle du cours : toute donnée qui change et qui est affichée est un signal (
signal(),computed(),input(),model()). Une propriété ordinaire convient aux valeurs fixes (une liste de choix, un texte d'aide).
-
L'interpolation
{{ }}en détail- L'interpolation évalue une expression et l'affiche sous forme de texte. Les versions récentes d'Angular y acceptent de nombreuses syntaxes de TypeScript : chaînage optionnel
?., coalescence??, gabarits de chaîne`…${ }…`, opérateursin,typeofet**.
- L'interpolation évalue une expression et l'affiche sous forme de texte. Les versions récentes d'Angular y acceptent de nombreuses syntaxes de TypeScript : chaînage optionnel
import { Component, signal } from '@angular/core';
interface Produit {
nom: string;
prix: number;
remise?: number;
fournisseur: { nom: string; ville: string } | null;
etiquettes: string[];
}
@Component({
selector: 'app-fiche-produit',
template: `
<h3>{{ produit().nom.toUpperCase() }}</h3>
<p>Prix : {{ produit().prix }} DT, soit {{ produit().prix * (1 - (produit().remise ?? 0)) }} DT après remise</p>
<p>Fournisseur : {{ produit().fournisseur?.nom ?? 'non renseigné' }}</p>
<p>{{ \`\${produit().etiquettes.length} étiquette(s) : \${produit().etiquettes.join(', ')}\` }}</p>
<p>{{ 'remise' in produit() ? 'En promotion' : 'Prix normal' }} – type du prix : {{ typeof produit().prix }}</p>
<p>{{ 2 ** 10 }} octets dans 1 Kio – {{ estCher() ? 'cher' : 'abordable' }}</p>
<p>{{ description }}</p>
`,
})
export class FicheProduit {
protected readonly produit = signal<Produit>({
nom: 'Clavier mécanique',
prix: 120,
remise: 0.25,
fournisseur: null,
etiquettes: ['bureautique', 'USB-C'],
});
protected readonly description = '<b>Livraison offerte</b>';
protected estCher(): boolean {
return this.produit().prix > 100;
}
}
CLAVIER MÉCANIQUE
Prix : 120 DT, soit 90 DT après remise
Fournisseur : non renseigné
2 étiquette(s) : bureautique, USB-C
En promotion – type du prix : number
1024 octets dans 1 Kio – cher
<b>Livraison offerte</b>
fournisseur?.nom ?? 'non renseigné':?.renvoieundefinedau lieu de planter sifournisseurvautnull, et??fournit alors un texte de remplacement. Sans?., la compilation échoue :TS2531: Object is possibly 'null'.- La dernière ligne affiche les balises telles quelles : l'interpolation échappe toujours le HTML. C'est une protection contre l'injection de code (XSS).
- Une interpolation n'a accès qu'aux membres de la classe :
{{ Math.round(prix) }}est refusé (TS2339: Property 'Math' does not exist on type 'FicheProduit'.). Faites le calcul dans uncomputed, une méthode ou un pipe. - Attention : une méthode appelée dans le template (
estCher()) est réexécutée à chaque vérification du composant. Gardez-la très simple ; pour un calcul plus lourd, préférez uncomputed, qui mémorise son résultat.
-
Liaison de propriété : propriété du DOM ou attribut HTML ?
- Les crochets
[ ]lient une propriété du DOM, c'est-à-dire une propriété de l'objet JavaScript qui représente l'élément, et non l'attribut écrit dans le HTML. - L'attribut donne la valeur de départ, toujours sous forme de chaîne ; la propriété contient la valeur actuelle, avec son vrai type (booléen, nombre…).
- Les crochets

import { Component, signal } from '@angular/core';
@Component({
selector: 'app-proprietes-attributs',
template: `
<button disabled="false">disabled="false"</button>
<button [disabled]="false">[disabled]="false"</button>
<button [disabled]="!accepte()">Valider</button>
<label><input type="checkbox" [checked]="accepte()" (change)="accepte.set(!accepte())" /> J'accepte</label>
<input #champ value="Tunis" [placeholder]="aide" />
<button (click)="lire(champ)">Lire le champ</button>
<p>{{ lecture() }}</p>
<progress [value]="inscrits" [max]="places"></progress>
<table><tr><td [attr.colspan]="2" [attr.aria-label]="'Total : ' + inscrits">Total</td></tr></table>
<p [hidden]="!accepte()">Merci !</p>
<div [innerHTML]="html"></div>
<p [textContent]="html"></p>
`,
})
export class ProprietesAttributs {
protected readonly accepte = signal(false);
protected readonly lecture = signal('');
protected readonly aide = 'Votre ville';
protected readonly inscrits = 18;
protected readonly places = 24;
protected readonly html = '<b>Inscriptions ouvertes</b> <img src="x" onerror="alert(1)"><script>alert(2)</script>';
protected lire(champ: HTMLInputElement): void {
this.lecture.set(`propriété value = « ${champ.value} », attribut value = « ${champ.getAttribute('value')} »`);
}
}
- Le premier bouton est désactivé malgré
disabled="false": pour HTML, la seule présence de l'attribut suffit.[disabled]="false"donne au contraire un bouton actif, car il lie la propriété booléenne. - Après avoir tapé « Sfax » puis cliqué sur Lire le champ, la page affiche
propriété value = « Sfax », attribut value = « Tunis »: l'attribut n'a pas bougé. colspann'a pas de propriété du même nom (elle s'appellecolSpan) : on lie l'attribut avec[attr.colspan]. Les attributs ARIA (aria-label…) peuvent s'écrire directement,[aria-label], ou avec[attr.aria-label].[innerHTML]insère du HTML, mais Angular le nettoie: ici, la balise<script>et l'attributonerrorsont supprimés, et la console afficheWARNING: sanitizing HTML stripped some content.[textContent], lui, affiche le texte brut, comme l'interpolation.
-
Lier des classes CSS et des styles
- Angular propose plusieurs formes de liaison pour les classes et les styles. Elles se combinent avec les classes et styles écrits en dur :
class="remplissage" [class]="niveau()"garde la classeremplissage.
- Angular propose plusieurs formes de liaison pour les classes et les styles. Elles se combinent avec les classes et styles écrits en dur :

import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-jauge',
template: `
<div class="jauge">
<div class="remplissage" [class]="niveau()" [style.width.%]="taux()"></div>
</div>
<p class="etat" [class.alerte]="niveau() !== 'ouvert'" [class.gras]="niveau() === 'complet'">
{{ inscrits() }} / {{ places }} inscrits ({{ taux() }} %)
</p>
<p [class]="['badge', niveau()]" [style]="{ 'font-size': 12 + inscrits() / 4 + 'px' }">{{ niveau() }}</p>
<button (click)="inscrire()" [disabled]="niveau() === 'complet'">+1 inscrit</button>
<button (click)="inscrits.set(12)">Réinitialiser</button>
`,
styles: `
.jauge { width: 320px; height: 14px; background: #e5e7eb; border-radius: 7px; overflow: hidden; }
.remplissage { height: 100%; transition: width .3s; }
.ouvert { background: #16a34a; } .presque { background: #f59e0b; } .complet { background: #dc2626; }
.alerte { color: #b45309; } .gras { font-weight: 700; color: #dc2626; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; }
`,
})
export class Jauge {
protected readonly places = 24;
protected readonly inscrits = signal(12);
protected readonly taux = computed(() => Math.round((this.inscrits() / this.places) * 100));
protected readonly niveau = computed(() => (this.taux() >= 100 ? 'complet' : this.taux() >= 75 ? 'presque' : 'ouvert'));
protected inscrire(): void {
this.inscrits.update((n) => Math.min(n + 1, this.places));
}
}
- Après six clics sur +1 inscrit, la page affiche
18 / 24 inscrits (75 %): la barre passe à l'orange (class="remplissage presque"), le texte reçoit la classealerteet le badge grossit (font-size: 16.5px). À 24 inscrits, tout passe au rouge et le bouton se désactive. [style.width.%]ajoute l'unité ; sans unité,[style.width]="75"est ignoré sans aucun message. Dans un objet[style]="{ … }", l'unité s'écrit dans la valeur ('18px') : la clé'font-size.px'n'y fonctionne pas.- Pour une seule classe ou un seul style, préférez
[class.nom]et[style.propriete]: ils sont plus lisibles et plus précis.
-
La liaison d'événement en détail
(evenement)="instruction"exécute l'instruction à chaque événement. La variable$eventcontient l'objet événement du navigateur :MouseEventpour un clic,KeyboardEventpour une touche…- Les filtres de touches réagissent à une touche précise, éventuellement combinée avec
control,shift,altoumeta. Un filtre est exact :(keydown.enter)ne réagit pas à Ctrl + Entrée.

import { Component, signal } from '@angular/core';
@Component({
selector: 'app-evenements-avances',
template: `
<input #tache (keyup.enter)="ajouter(tache)" (keyup.escape)="tache.value = ''" placeholder="Tâche puis Entrée" />
<ul>
@for (t of taches(); track $index) {
<li (click)="selection.set(t)">
{{ t }} <button (click)="supprimer($index, $event)">✕</button>
</li>
}
</ul>
<p>Sélection : {{ selection() }}</p>
<textarea #zone (input)="message.set(zone.value)" (keydown.control.enter)="envoyer()"></textarea>
<p>{{ message().length }} / 140 caractères – {{ envoye() }}</p>
<div class="zone" (mousemove)="suivre($event)" (mouseleave)="position.set(null)">
{{ position() ? 'x = ' + position()!.x + ', y = ' + position()!.y : 'Survolez la zone' }}
</div>
<form (submit)="$event.preventDefault(); envoyer()">
<button>Envoyer (sans recharger la page)</button>
</form>
`,
styles: `.zone { width: 260px; height: 60px; background: #eef2ff; display: grid; place-items: center; }`,
})
export class EvenementsAvances {
protected readonly taches = signal(['Installer Node.js', 'Créer le projet']);
protected readonly selection = signal('aucune');
protected readonly message = signal('');
protected readonly envoye = signal('');
protected readonly position = signal<{ x: number; y: number } | null>(null);
protected ajouter(champ: HTMLInputElement): void {
const texte = champ.value.trim();
if (texte) {
this.taches.update((liste) => [...liste, texte]);
}
champ.value = '';
}
protected supprimer(index: number, evenement: MouseEvent): void {
evenement.stopPropagation();
this.taches.update((liste) => liste.filter((_, i) => i !== index));
}
protected suivre(evenement: MouseEvent): void {
this.position.set({ x: evenement.offsetX, y: evenement.offsetY });
}
protected envoyer(): void {
this.envoye.set(`Envoyé à ${new Date().toLocaleTimeString('fr-FR')}`);
}
}
Créer le projet ✕
Lancer ng serve ✕
Sélection : Créer le projet
7 / 140 caractères – Envoyé à 21:08:31
x = 40, y = 20
- Ce résultat est obtenu en ajoutant « Lancer ng serve » (Entrée), en cliquant sur « Créer le projet », puis sur le ✕ d'« Installer Node.js », en tapant « Bonjour » suivi de Ctrl + Entrée, et en survolant la zone.
- Le ✕ supprime une tâche sans la sélectionner:
stopPropagation()empêche le clic de remonter jusqu'au<li>. De même,preventDefault()empêche l'envoi du formulaire de recharger la page. - Dans une méthode, typez le paramètre selon l'événement (
evenement: MouseEvent) : l'autocomplétion propose alorsoffsetX,stopPropagation()… - Attention : Angular ne connaît le type exact de
$event.targetque pour un<input>. Sur un<textarea>,$event.target.valueest refusé (TS2531: Object is possibly 'null'.etTS2339: Property 'value' does not exist on type 'EventTarget'.) : lisez la valeur avec une variable de référence, comme#zonedans l'exemple.
-
La liaison bidirectionnelle
[( )]en détail[(x)]="donnee"n'est qu'un raccourci: Angular le transforme en[x]="donnee()"(envoyer la valeur) plus(xChange)="donnee.set($event)"(recevoir la nouvelle valeur). On le surnomme la « banane dans une boîte ».- Cette forme fonctionne avec la directive
ngModel(champs de formulaire), avec tout composant qui déclare unmodel(), et Angular 22 apporte une troisième solution pour les formulaires : les Signal Forms.
![[( )] = liaison de propriété + liaison d'événement](https://apcpedagogie.com/wp-content/uploads/2026/10/angular-banane-dans-boite.png)
import { Component, model, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-quantite',
template: `
<button type="button" (click)="valeur.update((v) => v - 1)" [disabled]="valeur() <= 1">−</button>
<strong>{{ valeur() }}</strong>
<button type="button" (click)="valeur.update((v) => v + 1)">+</button>
`,
})
export class Quantite {
readonly valeur = model(1);
}
@Component({
selector: 'app-commande',
imports: [FormsModule, Quantite],
template: `
<input [(ngModel)]="nom" placeholder="Nom" />
<input [ngModel]="ville()" (ngModelChange)="ville.set($event.toUpperCase())" placeholder="Ville" />
<select [(ngModel)]="formation">
@for (f of formations; track f.code) {
<option [ngValue]="f">{{ f.titre }}</option>
}
</select>
@for (m of ['présentiel', 'en ligne']; track m) {
<label><input type="radio" name="mode" [value]="m" [(ngModel)]="mode" /> {{ m }}</label>
}
<input [(ngModel)]="commentaire" [ngModelOptions]="{ updateOn: 'blur' }" placeholder="Commentaire" />
<app-quantite [(valeur)]="places" />
<p>{{ nom() }} ({{ ville() }}) : {{ places() }} place(s) en {{ formation().titre }}, {{ mode() }}, {{ formation().prix * places() }} DT</p>
<p>Commentaire : {{ commentaire() || '—' }}</p>
`,
})
export class Commande {
protected readonly formations = [
{ code: 'NG', titre: 'Angular', prix: 450 },
{ code: 'TS', titre: 'TypeScript', prix: 300 },
];
protected readonly nom = signal('Amira');
protected readonly ville = signal('SOUSSE');
protected readonly formation = signal(this.formations[0]);
protected readonly mode = signal('présentiel');
protected readonly commentaire = signal('');
protected readonly places = signal(2);
}
Amira (SOUSSE) : 2 place(s) en Angular, présentiel, 900 DT
(après avoir tapé monastir, choisi TypeScript et en ligne, puis cliqué sur +)
Amira (MONASTIR) : 3 place(s) en TypeScript, en ligne, 900 DT
- La forme longue
[ngModel]+(ngModelChange)permet de transformer la saisie au passage : la ville est mise en majuscules à chaque frappe. [ngValue]lie un objet entier à une option ;[value]ne transmettrait qu'une chaîne. Les boutons radio d'un même groupe partagent le mêmenameet le mêmengModel.[ngModelOptions]="{ updateOn: 'blur' }"ne met la donnée à jour qu'à la sortie du champ : le commentaire n'apparaît qu'après avoir quitté la zone de saisie.<app-quantite [(valeur)]="places" />fonctionne parce que l'enfant déclarevaleur = model(1): ses boutons modifient la valeur, et le parent la reçoit aussitôt.- Attention : entre les guillemets de
[( )], on passe le signal lui-même, sans parenthèses.[(ngModel)]="nom()"est refusé :NG5002: Unsupported expression in a two-way binding.
-
Les Signal Forms d'Angular 22
- Les Signal Forms, stables depuis Angular 22, relient un formulaire entier à un signal. La fonction
form()crée un champ pour chaque propriété du modèle, avec ses règles de validation, et la directive[formField]lie chaque champ de saisie dans les deux sens. - Chaque champ expose son état sous forme de signals :
value(),valid(),invalid(),touched(),errors()… Ils s'importent depuis@angular/forms/signals.
- Les Signal Forms, stables depuis Angular 22, relient un formulaire entier à un signal. La fonction
import { Component, signal } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { FormField, email, form, min, minLength, required } from '@angular/forms/signals';
interface Inscription {
nom: string;
email: string;
age: number;
}
@Component({
selector: 'app-inscription',
imports: [FormField, JsonPipe],
template: `
<label>Nom <input [formField]="inscription.nom" /></label>
@if (inscription.nom().touched() && inscription.nom().invalid()) {
<small>{{ inscription.nom().errors()[0].message }}</small>
}
<label>E-mail <input type="email" [formField]="inscription.email" /></label>
@if (inscription.email().touched() && inscription.email().invalid()) {
<small>{{ inscription.email().errors()[0].message }}</small>
}
<label>Âge <input type="number" [formField]="inscription.age" /></label>
@if (inscription.age().invalid()) {
<small>{{ inscription.age().errors()[0].message }}</small>
}
<button [disabled]="inscription().invalid()" (click)="valider()">S'inscrire</button>
<pre>{{ modele() | json }}</pre>
<p>{{ resultat() }}</p>
`,
})
export class InscriptionForm {
protected readonly modele = signal<Inscription>({ nom: '', email: '', age: 18 });
protected readonly inscription = form(this.modele, (champ) => {
required(champ.nom, { message: 'Le nom est obligatoire' });
minLength(champ.nom, 3, { message: 'Au moins 3 caractères' });
required(champ.email, { message: "L'e-mail est obligatoire" });
email(champ.email, { message: 'E-mail invalide' });
min(champ.age, 16, { message: 'Âge minimum : 16 ans' });
});
protected readonly resultat = signal('');
protected valider(): void {
this.resultat.set(`Bienvenue ${this.modele().nom} !`);
}
}
(après avoir saisi Sa, sami@ et 15, en quittant chaque champ)
Au moins 3 caractères
E-mail invalide
Âge minimum : 16 ans
{ "nom": "Sa", "email": "sami@", "age": 15 } ← le bouton S'inscrire reste désactivé
(après avoir corrigé : Sami, sami@mail.tn, 20, puis clic sur S'inscrire)
Bienvenue Sami !
- Le modèle
modele()est mis à jour à chaque frappe, et le champagereçoit un vrai nombre grâce àtype="number". inscription().invalid()(avec des parenthèses aprèsinscription) donne l'état du formulaire entier;inscription.nom().invalid()celui d'un seul champ.- Les formulaires seront étudiés en détail dans un prochain chapitre ; retenez ici que
[formField]est une liaison bidirectionnelle qui ajoute la validation.
-
Aide-mémoire

-
Activité 1 : une fiche de formation
- Générer le composant
act-binding/fiche-formationqui affiche une formation stockée dans un signal :titre,image,prix,remisefacultative (0,2 pour −20 %),places,inscritsetformateur(un objet{ nom }ounull). La fiche doit :- afficher l'image avec
[src]et un texte alternatif construit avec le titre ; - afficher « Formateur : à confirmer » quand
formateurvautnull(?.et??) ; - barrer le prix d'origine quand il y a une remise (
[class.barre]) et afficher le prix remisé, calculé dans uncomputed; - afficher un badge « N place(s) restante(s) » vert, orange (3 places ou moins) ou rouge (complet), avec
[class]; - désactiver le bouton S'inscrire quand c'est complet, changer son texte en « Complet », lui donner un
aria-label, et encadrer la fiche en rouge ([class.complete]).
- afficher l'image avec
- Générer le composant

- Avec 12 places et 9 inscrits, combien de clics faut-il pour que le bouton se désactive ? Vérifier qu'un clic sur un bouton désactivé ne déclenche plus
inscrire(). - Remplacer
[disabled]="complet()"pardisabled="{{ complet() }}": que constatez-vous au chargement ? Expliquer à l'aide de la différence entre attribut et propriété.
-
Activité 2 : un aperçu de texte
- Générer le composant
act-binding/apercu-textequi règle l'apparence d'un paragraphe « Bienvenue au cours Angular » :- un curseur (
type="range", de 12 à 40) pour la taille, un sélecteur de couleur (type="color"), deux cases Gras et Italique, et une liste Alignement (left,center,right) ; - chaque réglage est un signal, lié au champ par
[value]ou[checked]et mis à jour par(input)ou(change)— sansngModel; - le paragraphe utilise
[style.font-size.px],[style.color],[style.text-align],[class.gras]et[class.italique]; - un bloc
<code>affiche le CSS équivalent, calculé dans uncomputed.
- un curseur (
- Générer le composant

- Pourquoi écrit-on
+$event.target.valuepour la taille, mais pas pour la couleur ? - Remplacer
[style.font-size.px]="taille()"par[style.font-size]="taille()": que se passe-t-il ? Proposer deux corrections.
-
Activité 3 : un mini-chat au clavier
- Générer le composant
act-binding/mini-chat: une liste de messages (texte, heure, importance) et un champ de saisie dont le brouillon est un signal. Gérer :- Entrée: envoyer le message (ignoré s'il est vide) ; Ctrl + Entrée: l'envoyer comme important (fond jaune,
[class.important]) ; - Échap: vider le brouillon ; flèche haut: rappeler le dernier message envoyé ;
- un clic sur un message le sélectionne (cadre bleu) ; un bouton ✕ le supprime sans le sélectionner (
stopPropagation()), avec un[attr.aria-label]« Supprimer le message N » ; - le nombre de caractères restants (80 au maximum,
maxlength), affiché en rouge sous 20, et un bouton Envoyer désactivé quand le brouillon est vide ; - le message « Aucun message » quand la liste est vide (
@empty).
- Entrée: envoyer le message (ignoré s'il est vide) ; Ctrl + Entrée: l'envoyer comme important (fond jaune,
- Générer le composant

- Retirer
evenement.stopPropagation(): que se passe-t-il quand on supprime un message ? Pourquoi ? - Que se passe-t-il si l'on écrit
(keydown.enter)et(keydown.control.enter)sur le même champ, puis que l'on tape Ctrl + Entrée ? Vérifier que le message n'est envoyé qu'une fois.
-
Activité 4 : une réservation avec
ngModel- Générer le composant
act-binding/reservation(avecFormsModule) pour réserver un séjour :- un nom mis en majuscule à chaque mot pendant la frappe (forme longue
[ngModel]+(ngModelChange)) ; - le nombre de personnes (1 à 10), une formule choisie dans une liste d'objets (
[ngValue]) : Demi-pension à 90 DT ou Pension complète à 130 DT par personne ; - une case Petit-déjeuner (+15 DT par personne) et deux boutons radio vendredi / samedi;
- un récapitulatif et un total calculés dans un
computed, et un bouton Réinitialiser qui remet toutes les valeurs de départ.
- un nom mis en majuscule à chaque mot pendant la frappe (forme longue
- Générer le composant

- Vérifier le total de 435 DT par le calcul. Que montre l'affichage après un clic sur Réinitialiser: pourquoi les champs se vident-ils eux aussi ?
- Remplacer
[ngValue]="f"par[value]="f": quelle valeur reçoit le signalformuleaprès un changement de formule ? Que deviennent le récapitulatif et le total ? Pourquoi aucune erreur n'est-elle signalée ?
-
Activité 5 : une inscription avec les Signal Forms
- Générer le composant
act-binding/inscriptionqui crée un compte avecform()et[formField]: nom, e-mail, mot de passe, confirmation, âge et case « J'accepte les conditions ». Règles :- nom obligatoire, au moins 3 caractères ; e-mail obligatoire et valide ; mot de passe d'au moins 8 caractères ; âge minimum 16 ans ; case obligatoire ;
- la confirmation doit être identique au mot de passe : écrire cette règle avec
validate(), en lisant l'autre champ avecvalueOf(c.motDePasse)et en renvoyant{ kind: 'different', message: '…' }; - un champ en erreur est encadré en rouge et affiche son premier message, mais seulement après avoir été quitté (
touched()) — écrire une méthodeaErreur(champ: FieldTree<unknown>); - le bouton Créer mon compte reste désactivé tant que le formulaire est invalide ; un clic affiche « Compte créé pour e-mail ».
- Générer le composant

- Pourquoi les messages d'erreur n'apparaissent-ils pas dès l'ouverture de la page, alors que le formulaire est déjà invalide ?
- Saisir des valeurs correctes partout sauf la case des conditions : le bouton est-il actif ? Quelle règle l'en empêche ?
- Faire un commit après chaque activité, puis un push :
git commit -m "Data binding : activité N".
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
TS2339: Property 'Math' does not exist on type 'FicheProduit'. |
Le template n'a accès qu'aux membres de la classe | Calculer dans un computed, une méthode ou un pipe |
TS2531: Object is possibly 'null'. sur {{ fournisseur.nom }} |
Objet qui peut valoir null |
fournisseur?.nom ?? 'texte', ou un @if |
NG5002: Parser Error: Bindings cannot contain assignments |
Affectation dans une interpolation ou une liaison de propriété | Modifier les données dans une liaison d'événement ou une méthode |
NG8002: Can't bind to 'colspan' since it isn't a known property of 'td'. |
Attribut HTML sans propriété du DOM correspondante | [attr.colspan] |
Bouton désactivé malgré disabled="false", élément caché par [hidden]="'false'" |
Attribut présent, ou chaîne non vide évaluée comme vraie | [disabled]="false", [hidden]="false" (sans apostrophes) |
[style.width]="75" sans effet, sans message |
Valeur sans unité | [style.width.px] ou [style.width.%] |
WARNING: sanitizing HTML stripped some content (console) |
[innerHTML] contenait du code dangereux (script, onerror…) |
Normal : Angular protège la page. N'insérer que du HTML de confiance |
TS2339: Property 'value' does not exist on type 'EventTarget'. |
$event.target.value sur un élément autre que <input> |
Variable de référence (#zone puis zone.value) |
TS2365: Operator '+' cannot be applied to types 'WritableSignal<number>' and 'number'. |
compteur++ sur un signal |
compteur.update(n => n + 1) dans une méthode |
NG5002: Unsupported expression in a two-way binding |
[(ngModel)]="nom()": signal appelé |
Passer le signal sans parenthèses : [(ngModel)]="nom" |
NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'. |
FormsModule absent des imports |
Ajouter FormsModule dans imports |
NG8002: Can't bind to 'formField' since it isn't a known property of 'input'. |
FormField absent des imports |
Importer FormField depuis @angular/forms/signals |
NG8109: nom is a function and should be invoked: nom() (avertissement) |
Signal lu sans parenthèses dans une liaison sens unique | {{ nom() }}, [value]="nom()" |
| Valeur qui ne s'affiche plus, puis « saute » au premier clic | Propriété ordinaire modifiée hors événement (minuteur, promesse) | En faire un signal |
-
À retenir
- Le data binding relie la classe et le template :
{{ }}et[ ]de la classe vers le template,( )du template vers la classe,[( )]dans les deux sens. - Angular 22 met à jour l'affichage quand un signal lu change, quand un événement lié se produit ou quand une entrée change : stockez les données affichées dans des signals.
- L'interpolation affiche du texte échappé et accepte
?.,??, les gabarits de chaîne,in,typeof; elle ne peut ni affecter ni utiliser des objets globaux commeMath. [ ]lie une propriété du DOM typée ;[attr.x]un attribut ;[class.x],[class],[style.x.unité],[style]les classes et styles.( )fournit$event, les filtres de touches (keydown.control.enter…),preventDefault()etstopPropagation().[(x)]="s"=[x]="s()"+(xChange)="s.set($event)": avecngModel, avecmodel(), ou avec[formField]pour un formulaire validé.
- Le data binding relie la classe et le template :
Les données vivent dans la classe, l'affichage dans le template, et le data binding fait circuler l'information entre les deux. Avec des signals, Angular sait exactement quoi mettre à jour.