model() crée un signal qui est à la fois input et output. Le composant peut y écrire (set/update), et l’écriture parvient automatiquement au parent via l’événement nameChange. C’est la base de la syntaxe bidirectionnelle [( )] (« banana in a box »).
Exemple
import { Component, input, model } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `
<button role="switch" [attr.aria-checked]="checked()" (click)="checked.set(!checked())">
<span class="knob"></span>
</button>
{{ label() }}
`,
})
export class Toggle {
// model() = input + output « checkedChange », dans un seul signal modifiable
readonly checked = model(false);
readonly label = input('');
}{
"notifications": false,
"darkMode": true,
"guests": 2,
"plan": "Free"
}model.required
@Component({ selector: 'app-stepper', /* ... */ })
export class Stepper {
readonly value = model.required<number>(); // le parent DOIT fournir une valeur
readonly min = input(0);
readonly max = input(10);
increment() {
this.value.update((v) => Math.min(v + 1, this.max())); // écriture → notifie le parent
}
}model() ou input + output ?
// model() : l’état « appartient » aux deux — le contrôle le modifie, le parent peut le définir
readonly selected = model<string>();
// input + output : l’enfant ne fait que PROPOSER, le parent décide (il peut refuser / valider / enregistrer sur le serveur)
readonly value = input.required<number>();
readonly valueChange = output<number>();
// parent : (valueChange)="confirmAndSave($event)"- Contrôles de « valeur » : toggle, slider, stepper, sélecteur de couleur, sélecteur de date.
- État d’interface que le parent peut vouloir contrôler : onglet actif, panneau déplié, dialogue ouvert.
- L’équivalent simple de
ngModelpour vos propres composants.
- Le parent doit valider ou confirmer la modification (p. ex. l’enregistrer sur le serveur).
- Les événements qui ne signifient pas « ma valeur a changé » :
(save),(delete). - Champs d’un formulaire Signal Forms → implémentez
FormValueControl(voir Custom controls) — toujours avecmodel(), mais intégré à la validation.
Ce que model() crée vraiment
Un champ checked = model(false) déclare deux choses sur le composant : un input nommé checked et un output nommé checkedChange. La syntaxe [(checked)] n’est qu’un raccourci pour les deux. C’est pourquoi le parent peut choisir : two-way, input seul ou output seul.
export class Toggle {
readonly checked = model(false);
// Ce qu’Angular voit sur le composant :
// input: checked
// output: checkedChange
}
<!-- les deux mêmes liaisons, écrites de quatre façons -->
<app-toggle [(checked)]="enabled" />
<app-toggle [checked]="enabled()" (checkedChange)="enabled.set($event)" />
<app-toggle [checked]="true" /> <!-- input seul -->
<app-toggle (checkedChange)="log($event)" /> <!-- output seul -->Un détail qui surprend : si le parent lie une valeur fixe ([checked]="true"), l’enfant peut quand même changer sa valeur avec set(). Le changement reste local jusqu’à ce que le parent envoie une nouvelle valeur, qui l’écrase. Si le parent doit être la seule source de vérité, liez un signal avec [( )].
Objets et tableaux dans model()
Comme tout signal, model() ne signale un changement que lorsqu’il reçoit une nouvelle référence. Si vous modifiez un champ de l’objet sur place, ni le parent ni le template ne le savent. Écrivez toujours une copie, avec update().
export class AddressEditor {
readonly address = model<Address>({ street: '', city: '' });
setCity(city: string) {
// ✗ mutation : personne ne le sait, le parent garde l’ancienne valeur
this.address().city = city;
// ✓ nouvelle référence : le template et le parent se mettent à jour
this.address.update((a) => ({ ...a, city }));
}
}Les options de model()
model() n’a que deux options : alias (le nom public ; l’output devient aliasChange) et debugName (pour Angular DevTools). Il n’a pas de transform comme input(). S’il faut transformer la valeur reçue, par exemple une chaîne du HTML en nombre, utilisez input() avec transform plus un output() séparé.
Pièges fréquents
[(checked)]="notifications()"— avec les parenthèses — n’est pas bidirectionnel : vous liez une valeur, pas le signal. Écrivez[(checked)]="notifications".- Un signal en lecture seule dans le parent (un
computed) dans[( )]→ erreur de compilation. Le binding bidirectionnel exige unWritableSignalou une propriété simple. - Deux « propriétaires » de l’état : si le parent et l’enfant appliquent chacun de la logique à la même valeur, le code devient difficile à suivre. Décidez qui est la source de vérité.
Aide-mémoire
model<T>() // ModelSignal<T | undefined>
model(default) // ModelSignal<T>
model.required<T>()
m.set(v); m.update(fn); // écrit en local + émet `${name}Change`
<app-x [(value)]="sig" /> // bidirectionnel sur un signal
<app-x [value]="v" (valueChange)="onChange($event)" />