Skip to main content

Command Palette

Search for a command to run...

Angular Signal Forms : Moins de complexité, plus de contrôle

Updated
18 min read
Angular Signal Forms : Moins de complexité, plus de contrôle

Les sujets que l’on peut trouver dans cet article :

  • Comment l’évolution de la gestion de state dans Angular reflète la gestion des formulaires

  • Vue d’ensemble des template driven forms

  • Vue d’ensemble des reactive forms

  • Vue d’ensemble des signal forms

  • Exploration de l’API des signal forms

  • Gestion des erreurs dans les signal forms

Dans le précédent article Signals, La Réactivité d’Angular Simplifiée, nous avons examiné le développement de la gestion de state dans Angular. Nous avons mentionné comment AngularJS reposait sur le two-way data binding pour le partage de state, puis nous sommes passés à Angular moderne qui a introduit une gestion structurée de state utilisant @Input et @Output pour la communication parent-enfant, ainsi que RxJS et des bibliothèques comme @ngrx/store pour imposer un flux de données unidirectionnel basé sur des observables.
Plus récemment, Angular Signals a simplifié la gestion de state en offrant une réactivité fine avec moins de boilerplate et un flux de données plus clair.

Nous commencerons par les template-driven forms, où le template était au premier plan et porteur de la logique, puis nous examinerons les reactive forms, où cette logique migre vers la classe, jusqu’aux signal forms, où le state du formulaire devient finement granulaire et plus réactif. Avec les signals, on observe une simplification de l’implémentation et un changement fondamental : la responsabilité de lire le state passe du template, à la classe, à une fonction fine-grained (signal) qui est elle-même un state qui réagit, notifie et compose le state.

Avant d’entrer dans une exploration approfondie de l’évolution de la gestion de l’état dans Angular, il est important de noter que les Signal Forms sont encore expérimentaux. Leur API n’est pas encore finalisée et peut évoluer entre les versions. La documentation disponible reste incomplète. En raison de cette maturité limitée, il convient de rester prudent aux changements si vous les utilisez dans des projets importants.

Template driven forms

Les template driven forms ont été introduits en 2016 dans la première version d’Angular2+. AngularJS avait un système de formulaires différent basé sur le two-way data binding. On retrouve la même idée dans les template driven forms, où l’API est un peu modernisée pour s’adapter à la direction d’Angular2+. Le two-way binding reste au cœur de l’idée, mais nous disposons maintenant de ngForm et ngModel « out of the box ». Mais comment cela fonctionne-t-il ?

La "logique" réside dans le template, car notre HTML définit le formulaire. ngForm et ngModel sont des directives qu’Angular instancie et configure pour le two-way binding. Ces directives analysent le DOM à l’initialisation du composant et mettent en place les form controls. Nous ne créons pas de variables pour stocker le state du formulaire dans notre classe et nous n’avons pas de formControl visible. Angular crée implicitement une instance de formControl pour chaque input via ngModel.

Le template est également responsable de la validation du formulaire, avec des directives comme required placées directement dans le template. La réactivité de notre formulaire est limitée et implicite. Les mises à jour (inputs utilisateur) se propagent automatiquement via ngModel, mais le flux lui-même n’est pas contrôlé. Angular met à jour le composant lorsque la détection des changements est déclenchée (lorsque l’utilisateur tape dans le formulaire), mais gérer les changements de manière programmatique devient plus complexe.

Faisons un rappel sur le fonctionnement des template driven forms :

import { Component } from "@angular/core";

export interface User {
  firstName: string;
  lastName: string;
}

@Component({
  selector: "my-app",
  templateUrl:`
  <h1>Template Driven Forms</h1>
  <form #userForm="ngForm">
    <input required [(ngModel)]="user.firstName" />
    <input required [(ngModel)]="user.lastName" />
    <button type="button" (click)="submit()">Submit</button>
  </form>

  <h3>Form value</h3>
  {{user | json}}

  <h3>Form states</h3>
  <p>Valid: {{userForm.valid}}</p>
  <p>Pristine:  {{userForm.pristine}}</p>
  <p>Touched:  {{userForm.touched}}</p>
  <p>Submitted: {{userForm.submitted}}</p>"`,
  styleUrls: ["./app.component.css"]
})

export class AppComponent {
  user: User = {
    firstName: "Jane",
    lastName: "Black"
  };

  submit() {
    alert(`
    Form submitted 
    with lastName ${this.user.lastName} 
    and firstName ${this.user.firstName}`
    );
  }
}

Nous ne déclarons pas notre formulaire dans le composant. Le template pilote les changements et contient le modèle du formulaire. Angular voit d’abord le formulaire avec #, puis instancie une directive ngForm. Ensuite, en lisant les inputs, il instancie la directive NgModel, qui effectue un two-way data binding avec le modèle de données (dans notre cas user) déclaré dans la classe du composant.

Les validateurs sont automatiquement liés aux contrôles. Pour assurer la connexion entre ngModel et le DOM, Angular s’appuie sur ControlValueAccessor, un mécanisme caché derrière les template driven forms et les reactive forms.

Voyons rapidement son interface :

export interface ControlValueAccessor {
  writeValue(obj: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(isDisabled: boolean): void;
}

On peut constater que c’est le ControlValueAccessor qui lit et écrit la valeur, tout en gérant également ses validations en enregistrant onChange et onTouched.

Les template driven forms reposent sur le template pour gérer le state et la validation, en utilisant ngForm et ngModel pour le two-way binding implicite. Elles sont simples à mettre en place, mais offrent une réactivité limitée et un contrôle programmatique moins précis.

Pour répondre à ces limites, Angular propose les reactive forms, qui permettent de définir explicitement le modèle de formulaire dans la classe du composant et de le connecter directement au template via FormControl. Examinons de plus près les mécanismes qui sous-tendent cette approche plus structurée et réactive.

Reactive forms

Dans les reactive forms d’Angular, le modèle évolue : ce n’est plus le template qui est responsable, mais la classe qui prend en charge le formulaire. Notre formulaire est maintenant défini explicitement dans la classe avec TypeScript. Le template se contente de rendre le modèle du formulaire, tandis que la « source de vérité » (source of truth) réside désormais dans notre fichier .ts.

Les reactive forms sont construits autour de flux observables et peuvent être consultés de manière asynchrone. Cela représente un changement par rapport aux template driven forms, où l’API ne fournissait pas de manière explicite un moyen de s’abonner aux changements d’un input.

C’est pourquoi, dans les reactive forms, nous pouvons nous abonner aux changements de la valeur d’un input.

form.valueChanges.subscribe(value => {
  console.log(value);
});

Notre formControl utilise toujours ControlValueAccessor, mais il peut maintenant être manipulé et utilisé de manière programmatique dans la classe de notre composant.
NgModel est remplacé par formGroup (ou formArray), créés explicitement dans le fichier .ts et qui contiennent le state. Tous les éléments des reactive forms étendent AbstractControl. Contrairement à la directive ngModel utilisée dans les template driven forms, les reactive forms étendent AbstractControl, qui est un objet représentant le state et les règles et qui n’a pas à se soucier de la vue.

Chaque AbstractControl expose deux observables que nous pouvons utiliser pour suivre la valeur de notre formulaire : valueChanges et statusChanges.
Mélanger modèle et vue, comme dans les template forms, rendait les tests difficiles et le state « caché » dans la directive difficile à déboguer. Avec les reactive forms, on observe une séparation plus claire entre modèle et vue : pour chaque modèle (AbstractControl), nous avons une directive côté vue et un ControlValueAccessor pour faire le lien entre les deux.

  • Model = FormControl / FormGroup / FormArray

  • Directive = [formControl], formControlName, formGroup, etc.

  • CVA = adaptateur vers le DOM

Examinons un exemple simple de reactive form :

import { Component } from "@angular/core";
import { FormGroup, FormControl, Validators } from "@angular/forms";

export interface User {
  firstName: string;
  lastName: string;
}

@Component({
  selector: "my-app",
  template:`
    <h1>Reactive Forms</h1>
    <form [formGroup]="userForm">
      <input formControlName="firstName"/>
      <input formControlName="lastName"/>
      <button type="button" (click)="submit()">Submit</button>
    </form>
    <h3>Form value</h3>
    {{ userForm.value | json }}

    <h3>Form states</h3>
    <p>Valid: {{ userForm.valid }}</p>
    <p>Pristine: {{ userForm.pristine }}</p>
    <p>Touched: {{ userForm.touched }}</p>
    <p>First name valid: {{ firstNameControl.valid }}</p>
    <p>First name invalid: {{ firstNameControl.invalid }}</p>
    <p>Last name valid: {{ lastNameControl.valid }}</p>
    <p>Last name invalid: {{ lastNameControl.invalid }}</p>`
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  userForm: FormGroup = new FormGroup({
    firstName: new FormControl('Jane', Validators.required),
    lastName: new FormControl('Black', Validators.required)
  });

  get firstNameControl() {
    return this.userForm.get('firstName') as FormControl;
  }

  get lastNameControl() {
    return this.userForm.get('lastName') as FormControl;
  }

  submit() {
    const user: User = this.userForm.value;
    alert(`
      Form submitted
      with lastName ${user.lastName}
      and firstName ${user.firstName}
    `);
  }
}

Le formulaire est désormais défini dans notre classe. La validité des contrôles peut être gérée de manière programmatique et la séparation entre la classe qui définit le formulaire et la vue, qui se contente de l’affichage, est claire.

Nous pouvons définir des fonctions getter en utilisant .get pour obtenir les valeurs de nos contrôles.
Comme les reactive forms étendent AbstractControl, nous avons accès à son API pour gérer et lire les valeurs ainsi que la validité de nos contrôles.

Rappelons-nous de certaines fonctionnalités ici :

abstract class AbstractControl<TValue = any, TRawValue extends TValue = TValue, TValueWithOptionalControlStates = any> {
  constructor(validators: ValidatorFn | ValidatorFn[] | null, asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null): AbstractControl<TValue, TRawValue, TValueWithOptionalControlStates>;
  readonly parent: FormGroup<any> | FormArray<any> | null;
  readonly valid: boolean;
  readonly invalid: boolean;
  readonly disabled: boolean;
  readonly enabled: boolean;
  readonly pristine: boolean;
  readonly dirty: boolean;
  readonly touched: boolean;
  readonly untouched: boolean;
  readonly valueChanges: Observable<TValue>;
  readonly statusChanges: Observable<FormControlStatus>;
}

Même si elles sont explicites, typées, testables et prévisibles, les reactive forms demandent également plus de boilerplate, car nous devons créer explicitement chaque contrôle dans la classe et le connecter au template. De plus, la dépendance à RxJS ajoute une complexité supplémentaire, et nous devons lire les valeurs explicitement via .value ou nous abonner aux changements. Elles stockent toujours le state du formulaire dans AbstractControl.

Même si la séparation entre la définition de la classe et la vue est plus claire, les reactive forms dépendent encore du CVA.

formControl -> CVA -> DOM

Cela signifie qu’un seul élément contrôle non seulement la lecture et l’écriture de la valeur, mais aussi l’enregistrement de l’état d’interaction avec onChange et onTouched. De plus, il est indirectement responsable de l’intégration de la validation, car c’est le CVA qui enregistre les callbacks déclenchant les mises à jour de valeur et d’état touché.

Les reactive forms déplacent la responsabilité du formulaire vers la classe, avec un modèle explicite défini via FormControl, FormGroup ou FormArray, tandis que le template se contente de l’affichage. Elles offrent une réactivité fine, des valeurs et états consultables via des observables (valueChanges, statusChanges) et une séparation claire entre modèle et vue, rendant tests et validation plus prévisibles. Cependant, elles demandent plus de code et reposent toujours sur le ControlValueAccessor pour gérer l’intégration avec le DOM et la validation, ce qui peut les rendre parfois difficiles à personnaliser et à déboguer.

Futur de la gestion des formulaires Angular : les Signal based forms

Poursuivant son orientation vers une gestion de state plus fine et plus réactive, Angular a introduit fin 2025 les signal based forms. L’introduction des signal based forms permet de résoudre le problème de surcharge cognitive et de code boilerplate tout en conservant l’abstraction côté modèle. Ainsi, au lieu de FormControl, FormGroup et FormArray, nous pouvons maintenant utiliser signal() et computed() pour obtenir un state réactif fin.

La réactivité est automatique : il n’est plus nécessaire de s’abonner explicitement avec valueChanges ou d’utiliser des async pipes, et les validations du formulaire sont auto-calculées et plus simples à gérer.

Les signals, introduits pour la première fois en 2023, permettent de définir le state du formulaire en utilisant des signals via des fonctions comme form() et d’accéder aux signals de chaque champ individuel pour des mises à jour, validations et bindings dans le template plus fins. Ils réduisent considérablement le besoin de connecter manuellement vos inputs et diminuent la quantité de code nécessaire pour que tout fonctionne.

Voyons un exemple de signal based form :


import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {form, Field} from '@angular/forms/signals';
interface UserForm {
  firstName: string;
  lastName: string;
  nicknames: string[];
}
@Component({
   selector: 'my-app',
  templateUrl: `<form>
    <label> First name
        <input [field]="loginForm.firstName" />
      </label>
    <label> Last name
      <input [field]="loginForm.lastName" />
      </label>
</form>
<p>Model</p>
<pre>{{ userFormModel() | json }}</pre>
<!-- {
  "firstName": "",
  "lastName": ""
} -->
<pre>{{ loginForm | json }}</pre>
<!-- we can't get values -->
</form>`,
  imports: [Field],
})
export class App {
// source of the truth
  userFormModel = signal<UserForm>({
    firstName: '',
    lastName: '',
    nicknames: ['bob', 'bob1', 'bob2']
  });

  // form metadata + validation
  loginForm = form(this.userFormModel);
}

Nous voyons apparaître de nouvelles fonctions à notre disposition, comme form(). Comprenons comment cela fonctionne.

L’API ressemble à ceci :

declare function form<TModel>(model: WritableSignal<TModel>): FieldTree<TModel>;

Ainsi, form() est une fonction à laquelle nous pouvons passer un modèle et qui nous renvoie un FieldTree de ce modèle. TModel est notre modèle, dans notre cas UserForm. WritableSignal(TModel) est la source de vérité pour les valeurs du formulaire. form() ne stocke pas les valeurs : il se lie simplement au signal, et c’est le signal qui possède les données. Le formulaire est uniquement responsable de la validation de la structure et des métadonnées.

C’est pourquoi nous ne pouvons pas lire les valeurs depuis loginForm, mais devons les lire depuis le signal userFormModel.

C’est dans FieldTree que la « magie » du formulaire se produit :

type FieldTree<TModel, TKey extends string | number = string | number> = (() => 
[TModel] extends [AbstractControl] ? CompatFieldState<TModel, TKey> : FieldState<TModel, TKey>) 
& 
([TModel] extends [AbstractControl] ? object : [TModel] extends [Array<infer U>] ? ReadonlyArrayLike<MaybeFieldTree<U, number>> : 
TModel extends Record<string, any> ? Subfields<TModel> : object);

Comme nous pouvons le voir, un fieldTree comporte à nouveau deux parties. Il agit à la fois comme une fonction qui renvoie le state d’un champ particulier, et comme un objet/array qui reflète la structure du modèle que nous lui avons passé.

La partie callable nous permet d’écrire quelque chose comme ceci :

loginForm()
loginForm.firstName()

Mais loginForm.firstName nous renverra le state du champ avec ses méthodes value(), valid(), touched(), etc. Si nous voulons obtenir la valeur, nous devons soit appeler le signal du modèle (signalModel), soit utiliser les valeurs du signal via form().

{{ loginForm.firstName() }} // will give field state
{{ loginForm.firstName().value() }}  // will give value

La partie callable confirme le fait que les Angular signals sont, dans leur essence, des fonctions. Cela nous permet de lire le state du formulaire de manière réactive, et le fait de l’appeler renvoie les métadonnées, et non les valeurs (à moins d’appeler value(), bien sûr).

La deuxième partie est la « partie contrat » qui indique à notre FieldTree de refléter la structure du modèle. Elle étend [AbstractControl] pour être compatible avec les formulaires Angular legacy, mais elle étend également les tableaux, objets et primitives :

& (
  TModel extends Array
  | Object
  | Primitive
)

Si notre modèle est un tableau, le formulaire devient :

{{ loginForm.nicknames().value() }} // 'bob', 'bob1', 'bob2'
{{ loginForm.nicknames().value()[0] }} // bob

Et pour nos objets :

TModel extends Record<string, any>
  ? Subfields<TModel>

Ce qui signifie :

FieldTree<UserForm> ≈ {
  firstName: FieldTree<string>; 
  lastName: FieldTree<string>;
}

Ainsi, chaque clé d’objet devient un FieldTree contenant toutes les métadonnées et signatures de type. Contrairement aux reactive forms où l’on peut appeler .value, ici .value est supprimé car ce n’est pas réactif et, techniquement, cela aurait permis d’avoir deux sources de vérité.

Plus tôt dans l’article, nous avons expliqué qu’avec les reactive forms, on observe une séparation plus claire entre modèle et vue. Le modèle (AbstractControl) vit dans la classe et, côté vue, nous avons une directive. Le ControlValueAccessor servait de pont entre les deux.

Les formulaires basés sur les signals n’utilisent pas FormControl, ngModel, ni l’ancienne API des formulaires — contrairement aux versions anciennes d’Angular qui reposaient toujours sur ControlValueAccessor.
Les signals ne dépendent pas de ControlValueAccessor, car le signal contient la valeur et le FieldTree contient les métadonnées du formulaire. Les signals assurent un suivi automatique des dépendances : lorsqu’un signal change, Angular sait quoi mettre à jour. Il s’agit d’une réactivité fine qui fait vraiment briller les signals. Les inputs personnalisés, au lieu d’implémenter CVA, se contentent de lier les signals.

Presque tous les développeurs Angular ont eu besoin d’écrire des inputs personnalisés. Avec les reactive forms, nous devions faire quelque chose comme ceci :

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'custom-input',
  template: ``,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true,
    },
  ],
})
export class CustomInputComponent implements ControlValueAccessor {
  private onChange = (value: number) => {};

  private onTouched = () => {};

  writeValue(value: number): void {
    this.value = value ?? 0;
  }

  registerOnChange(fn: (value: number) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setValue(value: number) {
    this.value = value;
    this.onChange(value);
    this.onTouched();   
  }
}

On voit que nous devions gérer la lecture, l’écriture et les scénarios d’interaction. Il fallait également implémenter ControlValueAccessor, utiliser forwardRef, etc.

Avec les signal forms et FormValueAccessor, on peut observer le même processus beaucoup plus simplifié :

import { Component, model } from '@angular/core';  
import { FormValueControl } from '@angular/forms';  
@Component({  
selector: 'app-custom-input',  
template: ``  
})  
export class CustomInputComponent implements FormValueControl<number> {  
    value = model<number>(0);
    const touched = signal(false);

    select(value: number): void {  
        this.value.set(rating);
    }  
}

C’est en réalité ce que les signals changent dans leur cœur.

Les signal based forms utilisent des signal() et computed() pour un state réactif, sans avoir besoin de FormControl ni de ControlValueAccessor. Les validations et mises à jour se propagent automatiquement, et chaque champ fournit ses métadonnées via FieldTree. Cette approche réduit le boilerplate et sépare clairement lecture, écriture et suivi des états d’interaction. Grâce aux signals, on observe une distinction nette entre la lecture et l’écriture des valeurs et l’enregistrement des états d’interaction, ce qui rend les signal forms particulièrement efficaces pour la validation des formulaires.

Gestion des erreurs dans les signal forms

La réactivité, la séparation plus claire entre lecture et écriture, ainsi que l’accès facile aux valeurs calculées sont particulièrement utiles pour la validation des formulaires et la gestion des erreurs. La réactivité fine signifie que chaque champ signal se met à jour de manière indépendante. Le code est plus simple, plus facile à écrire et à maintenir, et il est beaucoup plus facile de désactiver les boutons de soumission.

Considérez le code suivant :

import { Component, signal, ChangeDetectionStrategy, computed } from '@angular/core';
import {
  email,
  form,
  FormField,
  min,
  validate,
} from '@angular/forms/signals';

interface LoginData {
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
}

@Component({
  selector: 'app-root',
  templateUrl: '
<form style="display: flex; flex-direction: column; gap: 10px;">
  <label>
    Email:
    <input type="email" [formField]="loginForm.email" />
  </label>
  <label>
    Password:
    <input type="password" [formField]="loginForm.password" />
  </label>
  <label>
    Confirm Password
    <input [formField]="loginForm.confirmPassword" id="">
  </label>
  <label>
    Age
    <input [formField]="loginForm.age" type="number" id="">
  </label>
  <button (click)="submit()" style="max-width: fit-content;">Submit</button>
  <div class="error-list">
    <div>
      @for (error of loginForm.password().errors(); track error) {
      <p>{{ error.message }}</p>
      }
    </div>
    <div class="error-list">
      @for (error of loginForm.email().errors(); track error) {
      <p>{{ error.message }}</p>
      }
    </div>
    <div class="error-list">
      @for (error of loginForm.age().errors(); track error) {
      <p>{{ error.message }}</p>
      }
    </div>
  </div>
  <div class="error-list">
    @for (error of loginForm.confirmPassword().errors(); track error) {
    <p>{{ error.message }}</p>
    }
    <div class="error-list">
      <p> {{passwordStrength()}}</p>
    </div>
  </div>
</form>
',
  styleUrl: './app.css',
  imports: [FormField],
})
export class App {
  protected readonly title = signal('signals');

  loginModel = signal<LoginData>({
    email: '',
    password: '',
    confirmPassword: '',
    age: 0,
  });

  loginForm = form(this.loginModel, (s) => {
    email(s.email, { message: 'Email is needs to be valid' });
    min(s.age, 18, { message: 'Age needs to be more than 18' });
    validate(s.confirmPassword, ({ value, valueOf }) => {
      const confirmPassword = value(); 
      const password = valueOf(s.password);
      if (confirmPassword !== password) {
        return {
          kind: "error",
          message: 'Passwords do not match',
        };
      }
      return null;
    });
  });

  passwordStrength = computed(() => {
    const password = this.loginForm.password().value();
    const touched = this.loginForm.password().dirty();
    if (password.length < 8 && touched) return 'weak';
    if (password.length <= 12 && touched) return 'medium';
    if (password.length >= 12 && touched) return 'strong';
    return '';
  });
}

Dans cet exemple, on peut voir la nouvelle API des formulaires basés sur les signals en action, où tout le formulaire est réactif sans utiliser FormGroup traditionnel. Les signals ne suppriment pas la validation ni la logique du formulaire : ils déplacent simplement la responsabilité vers des primitives plus “fines”, permettant un contrôle plus précis et réactif sur chaque champ.

Le signal loginModel contient l’état du formulaire, et la fonction form() lui ajoute des règles de validation comme email(), min(), ainsi qu’une validation personnalisée avec validate() pour vérifier que les mots de passe correspondent. Le loginForm() retourné est lui-même un signal, ce qui signifie qu’il fournit des métadonnées réactives sur tout le formulaire — comme la validité, les erreurs, l’état dirty et les valeurs. Grâce à cela, on peut facilement contrôler le bouton Submit avec [disabled]="!loginForm().valid()". Dès que le formulaire est invalide, le bouton est désactivé automatiquement, et il se réactive immédiatement lorsque tout devient valide.

Des messages d’erreur s’affichent lorsque les règles de validation ne sont pas respectées (par exemple : email invalide, âge inférieur à 18, mot de passe trop court ou mots de passe différents). Dès que l’utilisateur corrige la saisie, les erreurs disparaissent automatiquement car les signals recalculent immédiatement l’état. Le signal calculé passwordStrength réagit aussi aux changements du mot de passe et met à jour la force en temps réel.

Vers l’avenir des formulaires Angular

L’évolution des formulaires dans Angular illustre un déplacement progressif de la responsabilité du state :

Avec les template-driven forms, le template pilote la logique et la validation. Le two-way binding implicite via ngModel et ngForm rend l’implémentation simple, mais la réactivité est limitée et le contrôle programmatique moins précis.

Les reactive forms déplacent le modèle dans la classe, avec des FormControl, FormGroup et FormArray. Cette approche offre une séparation claire entre modèle et vue, une réactivité fine via les observables valueChanges et statusChanges, et un contrôle programmatique complet. Cependant, elles demandent plus de code et reposent toujours sur le ControlValueAccessor, ce qui peut rendre la personnalisation et le débogage plus complexes.

Les signal forms, quant à elles, introduisent un state réactif fin-grained via signal() et computed(), sans nécessiter de FormControl ni de ControlValueAccessor. Chaque champ fournit ses métadonnées via FieldTree, et les validations ainsi que la propagation des valeurs sont automatiques. Les signal forms réduisent le boilerplate, séparent clairement lecture, écriture et suivi des états d’interaction, et rendent la gestion des erreurs et des boutons de soumission réactive et intuitive.

Cette évolution montre une direction claire : Angular se dirige vers des formulaires plus réactifs, plus simples à maintenir et plus précis dans le suivi des états. Cependant, les signal forms restent expérimentales : leur API est encore jeune et susceptible d’évoluer. Avant de les utiliser dans des projets critiques, il est essentiel de comprendre ces limites et de suivre la documentation officielle (Signal Forms – Angular).

Aujourd’hui, on peut voir la direction que prend Angular moderne : les signal forms représentent une étape prometteuse pour la gestion des formulaires, combinant réactivité fine, contrôle programmatique clair et simplification du code.

Documentation Angular Signal Forms : https://angular.dev/essentials/signal-forms

Article intéressant qui parle de formValueAccessor vs ControlValueAccessor : https://javascript.plainenglish.io/controlvalueaccessor-is-dead-long-live-formvaluecontrol-4cf2e30a4fb0

L’image: Photo de Linus Sandvide sur Unsplash

More from this blog

N

Niji.tech, c’est le meilleur de la tech by Niji

54 posts

Niji.tech, c’est le meilleur de la tech by Niji :
du web, du mobile, de l'agile, du design, de l'IA, des bonnes pratiques, et plus encore !