Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Le data binding dans Angular

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

0 chapitres 0 QCM Accès gratuit
Dans cet article

À 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.
  1. 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() et stopPropagation()
    • Maîtriser la liaison bidirectionnelle avec ngModel, model() et les Signal Forms d'Angular 22
  1. 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.

Les quatre formes de data binding entre la classe et le template

  • Pour suivre ce chapitre, continuez dans le projet premiers-pas-angular et créez un dossier src/app/binding/. Tous les exemples ont été testés avec Angular 22.
  1. 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égie OnPush par défaut. Une propriété ordinaire modifiée dans un minuteur n'est donc plus affichée automatiquement.

Un signal prévient Angular, une propriété ordinaire non

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 s mais toujours Proprié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 brusquement 3 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).
  1. 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érateurs in, typeof et **.
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é': ?. renvoie undefined au lieu de planter si fournisseur vaut null, 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 un computed, 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 un computed, qui mémorise son résultat.
  1. 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…).

Différence entre attribut HTML et propriété du DOM

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é.
  • colspan n'a pas de propriété du même nom (elle s'appelle colSpan) : 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'attribut onerror sont supprimés, et la console affiche WARNING: sanitizing HTML stripped some content. [textContent], lui, affiche le texte brut, comme l'interpolation.
  1. 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 classe remplissage.

Les différentes formes de liaison de classes et de styles

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 classe alerte et 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.
  1. La liaison d'événement en détail

    • (evenement)="instruction" exécute l'instruction à chaque événement. La variable $event contient l'objet événement du navigateur : MouseEvent pour un clic, KeyboardEvent pour une touche…
    • Les filtres de touches réagissent à une touche précise, éventuellement combinée avec control, shift, alt ou meta. Un filtre est exact : (keydown.enter) ne réagit pas à Ctrl + Entrée.

$event, filtres de touches, preventDefault et stopPropagation

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 alors offsetX, stopPropagation()…
  • Attention : Angular ne connaît le type exact de $event.target que pour un <input>. Sur un <textarea>, $event.target.value est refusé (TS2531: Object is possibly 'null'. et TS2339: Property 'value' does not exist on type 'EventTarget'.) : lisez la valeur avec une variable de référence, comme #zone dans l'exemple.
  1. 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 un model(), et Angular 22 apporte une troisième solution pour les formulaires : les Signal Forms.

[( )] = liaison de propriété + liaison d'événement

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ême name et le même ngModel.
  • [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éclare valeur = 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.
  1. 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.
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 champ age reçoit un vrai nombre grâce à type="number".
  • inscription().invalid() (avec des parenthèses après inscription) 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.
  1. Aide-mémoire

Aide-mémoire du data binding Angular

  1. Activité 1 : une fiche de formation

    • Générer le composant act-binding/fiche-formation qui affiche une formation stockée dans un signal : titre, image, prix, remise facultative (0,2 pour −20 %), places, inscrits et formateur (un objet { nom } ou null). La fiche doit :
      • afficher l'image avec [src] et un texte alternatif construit avec le titre ;
      • afficher « Formateur : à confirmer » quand formateur vaut null (?. et ??) ;
      • barrer le prix d'origine quand il y a une remise ([class.barre]) et afficher le prix remisé, calculé dans un computed;
      • 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]).

Résultat attendu de l'activité 1 après deux inscriptions

  • 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()" par disabled="{{ complet() }}": que constatez-vous au chargement ? Expliquer à l'aide de la différence entre attribut et propriété.
  1. Activité 2 : un aperçu de texte

    • Générer le composant act-binding/apercu-texte qui 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) — sans ngModel;
      • 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 un computed.

Résultat attendu de l'activité 2 : taille 28, rouge, gras, centré

  • Pourquoi écrit-on +$event.target.value pour 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.
  1. 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).

Résultat attendu de l'activité 3 avec un message important et un message sélectionné

  • 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.
  1. Activité 4 : une réservation avec ngModel

    • Générer le composant act-binding/reservation (avec FormsModule) 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.

Résultat attendu de l'activité 4 : 3 personnes en pension complète avec petit-déjeuner

  • 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 signal formule après un changement de formule ? Que deviennent le récapitulatif et le total ? Pourquoi aucune erreur n'est-elle signalée ?
  1. Activité 5 : une inscription avec les Signal Forms

    • Générer le composant act-binding/inscription qui crée un compte avec form() 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 avec valueOf(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éthode aErreur(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 ».

Résultat attendu de l'activité 5 avec des saisies invalides

  • 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".
  1. 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
  1. À 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 comme Math.
    • [ ] 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() et stopPropagation().
    • [(x)]="s" = [x]="s()" + (xChange)="s.set($event)": avec ngModel, avec model(), ou avec [formField] pour un formulaire validé.
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.