Retour à l'accueil

Angular 21 Signal Forms : Architecture et Typage

L’article détaille les Signal Forms dans Angular 21 : Primitives architecturales FieldTree et FieldState, Avantages du typage, Gestion de l’état des éléments (Hidden/Disabled/Readonly), Debounce intégré et Validation croisée des champs via ValidateTree.

Angular 21 : Signal Forms sans Abstractions
Advertisement 728x90

Formulaires à signaux Angular 21 : comment l’API Signal révolutionne la gestion des formulaires au niveau architectural

Angular 21 a introduit les formulaires à signaux — pas seulement un nouvel outil, mais un changement fondamental dans le paradigme de la gestion de l’état des formulaires. Il ne s’agit pas d’une simple évolution des formulaires réactifs ; c’est une refonte complète à travers le prisme de l’API Signal : synchronisation directe entre le modèle et l’interface utilisateur, typage strict sans perte de contexte, gestion déclarative de l’état des éléments du formulaire, ainsi qu’une prise en charge intégrée du debounce et de la validation inter-champs. Bien que cette fonctionnalité soit encore expérimentale, ses solutions architecturales définissent déjà la voie à suivre pour les applications d’entreprise exigeant une grande prévisibilité et une maintenance simplifiée.

Fondation architecturale : FieldTree et FieldState au lieu de FormGroup/FormControl

Les formulaires à signaux abandonnent l’abstraction AbstractControl au profit de deux primitives strictement typées :

  • FieldTree<T> — un objet proxy représentant l’arbre du formulaire sous forme de signaux. Chaque nœud est un Signal<FieldState<T>>, et l’accès à la valeur se fait via un appel ().
  • FieldState<T> — l’état d’un champ spécifique : valeur, statut (valid, touched, dirty), erreurs et méthodes de gestion (set, reset, markAsTouched).

Cela élimine le besoin de synchroniser manuellement les données de saisie (@Input) avec l’état interne du formulaire. Dans les formulaires réactifs, il fallait utiliser valueChanges combiné à patchValue, ce qui introduisait des risques de désynchronisation et compliquait la gestion du cycle de vie. Avec les formulaires à signaux, le modèle devient la source unique de vérité directement :

Google AdInline article slot
 export interface LoginFormModel {
  email: string;
  password: string;
}

@Component({
  imports: [Field],
  template: `
    <form>
      Email : <input [field]="loginForm.email">
      Password : <input [field]="loginForm.password">
    </form>
  `
})
export class LoginForm {
  login = model.required<LoginFormModel>();
  loginForm = form(this.login); // Relie le signal du modèle à l’arbre du formulaire
}

Ici, this.login est un WritableSignal<LoginFormModel>, et loginForm.email() renvoie un FieldState<string>. Modifier login.set({...}) met instantanément à jour tous les FieldState connectés, et inversement, modifier la valeur via loginForm.email().set(...) met à jour login().

Typage sans perte : de AbstractControl aux types précis des champs

L’avantage clé est la préservation de la structure de type à tous les niveaux. Dans les formulaires réactifs, get('nested.field') renvoie AbstractControl | null, ce qui fait perdre toute information sur la structure imbriquée. Avec les formulaires à signaux, le chemin path.additionalInformation.firstName a un type strict de FieldState<string>, tandis que path.additionalInformation() est un FieldState<{ firstName: string; lastName: string; }>. Cela permet d’utiliser en toute sécurité effect, computed et d’autres primitives de l’API Signal sans casting de type ni vérification de undefined.

Prenons l’exemple du groupe additionalInformation :

Google AdInline article slot
effect(() => {
  const info = this.loginForm.additionalInformation();
  console.log(info.firstName().value()); // string — le compilateur sait exactement
  console.log(info.lastName().touched()); // boolean
});

Un tel typage élimine les bugs classiques : tentatives d’appeler .value sur null, erreurs lors de la manipulation de FormGroup imbriqués et implicité any dans des chaînes comme get().get().value.

Gestion des états des éléments : hidden, disabled, readonly

Les formulaires à signaux offrent trois mécanismes intégrés pour contrôler la visibilité et l’interactivité, chacun avec sa propre sémantique :

  • hidden(path, condition?) — supprime complètement le champ du DOM (via @if) et de l’arbre de validation. Cela n’affecte pas touched/dirty puisque l’élément n’existe pas dans le formulaire.
  • disabled(path, condition?) — rend le champ inaccessible à l’édition et l’exclut de la validation ainsi que des états touched/dirty.
  • readonly(path, condition?) — bloque l’édition mais maintient le champ dans le cycle de validation et permet de suivre touched/dirty.

Les conditions sont définies via une fonction qui reçoit le contexte { valueOf }. Par exemple :

Google AdInline article slot
form<{ email: string; password: string }>( 
  { email: '', password: '' },
  (path) => {
    disabled(path.password, ({ valueOf }) => !valueOf(path.email));
  }
);``` 

Cela équivaut à `password.disabled = !email.value`, mais cela s’exécute de manière réactive et garantit la cohérence.

## Debounce et validation : solutions intégrées plutôt que RxJS

Pour contrôler la fréquence des mises à jour des données, la fonction `debounce` est implémentée, acceptant soit un délai en millisecondes, soit un `Debouncer` personnalisé :

debounce(path, (

{ valueOf },

abortSignal

) => {

return new Promise<void>((resolve) => {

const timeout = setTimeout(() => resolve(), valueOf(path.email).length * 100);

abortSignal.addEventListener('abort', () => clearTimeout(timeout));

});

});```

La validation est divisée en synchrone (required, minLength, pattern) et asynchrone (validateAsync, validateHttp). Une caractéristique spéciale est la prise en charge de validateTree, qui permet de créer des règles inter-champs. Par exemple, pour vérifier la correspondance des mots de passe :

validateTree(path, (ctx) => {
  const { password, confirmPassword } = ctx.value();
  if (password !== confirmPassword) {
    return {
      field: ctx.fieldTree.confirmPassword,
      kind: 'passwordMismatch',
      message: 'Les mots de passe ne correspondent pas'
    };
  }
});```

Ici, `ctx.fieldTree` donne accès à tous les `FieldState` dans l’arbre, et `ctx.value()` fournit la valeur actuelle de l’ensemble du formulaire.

## Ce qui compte

- Les formulaires à signaux ne remplacent pas les formulaires réactifs ; ils constituent une refonte architecturale sous l’API Signal : le modèle et le formulaire sont désormais une seule source de vérité.
- Le typage est préservé à tous les niveaux d’imbrication, éliminant `AbstractControl` et le besoin de vérifications manuelles de type `instanceof`.
- `hidden`, `disabled` et `readonly` ont une sémantique claire et gèrent automatiquement la validation ainsi que les états `touched/dirty`.
- `debounce` et `validateTree` sont intégrés au cœur, éliminant la dépendance à RxJS pour les scénarios de base.
- Le statut expérimental signifie que l’API peut évoluer ; son utilisation en production nécessite une compréhension claire des risques et un plan de migration.

Les formulaires à signaux d’Angular 21 représentent une étape vers une gestion des formulaires plus prévisible, plus sûre sur le plan des types et moins verbeuse. Ils ne simplifient pas les cas simples, mais réduisent radicalement la complexité dans les scénarios complexes : formulaires imbriqués, champs conditionnels, validation croisée et synchronisation dynamique. Pour les développeurs de niveau intermédiaire ou senior, ce n’est pas un outil « rapide à prendre en main » ; c’est une primitive architecturale qui vaut la peine d’être étudiée en profondeur, jusqu’au niveau de l’implémentation de `FieldTree` et du comportement de `effect` dans le contexte du formulaire.

— Editorial Team

Advertisement 728x90

Lire ensuite