~2 min de lecture
Angular 19 — Form Value Control : fini le ControlValueAccessor verbeux
Jusqu'ici, créer un composant de formulaire réutilisable imposait d'implémenter ControlValueAccessor. Résultat : un
provider à déclarer, trois méthodes obligatoires à câbler, et la gestion manuelle du disabled avec un markForCheck
au moindre oubli.
Angular 19 introduit une alternative Signal-first : FormValueControl. Moins de lignes, moins de risques, et le
comportement disabled géré automatiquement.
TL;DR
| ControlValueAccessor | FormValueControl | |
|---|---|---|
| Provider | NG_VALUE_ACCESSOR requis |
Aucun |
| API | 3–4 méthodes manuelles | 1 model() |
| Disabled | setDisabledState + markForCheck |
Automatique |
| Version | Toutes | Angular 19+ |
L'ancienne façon — ControlValueAccessor
Pour illustrer, voici un sélecteur de quantité classique :
@Component({
selector: 'app-quantity',
template: `
<button (click)="decrement()">-</button>
<span>{{ qty }}</span>
<button (click)="increment()">+</button>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => QuantityComponent),
multi: true,
},
],
})
export class QuantityComponent implements ControlValueAccessor {
protected qty = 0;
private onChange: (v: number) => void = () => {};
private onTouched: () => void = () => {};
writeValue(value: number): void {
this.qty = value ?? 0;
}
registerOnChange(fn: (v: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// il faut penser à injecter ChangeDetectorRef et appeler markForCheck()
}
increment(): void {
this.qty++;
this.onChange(this.qty);
this.onTouched();
}
decrement(): void {
this.qty--;
this.onChange(this.qty);
this.onTouched();
}
}
Beaucoup de plomberie pour un comportement assez simple.
La nouvelle façon — FormValueControl
Avec Angular 19, le même composant devient :
import { FormValueControl } from '@angular/forms';
@Component({
selector: 'app-quantity',
template: `
<button (click)="decrement()" [disabled]="disabled()">-</button>
<span>{{ value() }}</span>
<button (click)="increment()" [disabled]="disabled()">+</button>
`,
})
export class QuantityComponent implements FormValueControl<number> {
public value = model<number | null>(0);
public disabled = model<boolean>(false);
increment(): void {
if (!this.disabled()) this.value.update((v) => (v ?? 0) + 1);
}
decrement(): void {
if (!this.disabled()) this.value.update((v) => (v ?? 0) - 1);
}
}
- Plus de
providersà déclarer - Plus de callbacks
onChange/onTouchedà stocker disabledse synchronise automatiquement avec l'état du formulaire parent
Important :
valueetdisableddoivent être déclaréspublicpour satisfaire le contrat de l'interface — une déclarationprotectedproduit une erreur de compilation.
Côté parent — la structure formulaire
FormValueControl impose d'encapsuler le contrôle dans un objet de formulaire. Un FormControl isolé ne suffit plus :
@Component({
template: `
<form [formGroup]="form">
<app-quantity formControlName="qty" />
</form>
<button (click)="toggle()">Activer / Désactiver</button>
<p>Valeur : {{ form.value.qty }}</p>
`,
})
export class CartComponent {
protected readonly form = new FormGroup({
qty: new FormControl(1),
});
toggle(): void {
const ctrl = this.form.controls.qty;
ctrl.disabled ? ctrl.enable() : ctrl.disable();
}
}
Appuie sur le bouton → le composant enfant reflète instantanément l'état disabled, sans aucune détection de
changement manuelle.
Types spécialisés
Angular fournit déjà quelques interfaces dérivées pour les cas courants :
FormCheckboxControl— pour les cases à cocher (value: ModelSignal<boolean | null>)FormSelectControl<T>— pour les listes déroulantes
Si ton composant correspond à l'un de ces types, utilise l'interface spécialisée plutôt que FormValueControl<T> générique.
Faut-il migrer tout de suite ?
Pas forcément. ControlValueAccessor reste pleinement supporté et n'est pas deprecated. La migration a du sens si :
- tu crées un nouveau composant de formulaire (pas de raison de partir sur l'ancienne API)
- tu as un composant existant déjà bien testé et sans bug → reste sur CVA, la valeur ajoutée ne justifie pas le risque
- tu veux réduire la friction pour les développeurs juniors de ton équipe →
FormValueControlgagne clairement
Conclusion
FormValueControl élimine le principal point de friction de la création de composants personnalisés : plus de providers,
plus de callbacks, plus de détection de changement manuelle. L'interface est courte, lisible, et tire pleinement parti
des Signals.
Si tu avais l'habitude de contourner ControlValueAccessor par crainte de la complexité, c'est le bon moment pour
reconsidérer la chose.
Tu veux aller plus loin et maîtriser Angular de fond en comble — Signals, formulaires, architecture, tests ? C'est exactement ce que couvre EasyAngularKit, la formation Angular par la pratique.