# 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](https://niji.tech/signals-angular-reacting-simplified), 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 :

```typescript
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 :

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

```typescript
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 :

```typescript
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 :

```typescript
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 :

```typescript

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 :

```typescript
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 :

```typescript
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 :

```typescript
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()`.

```js
{{ 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 :

```js
& (
  TModel extends Array
  | Object
  | Primitive
)
```

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

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

Et pour nos objets :

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

Ce qui signifie :

```typescript
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 :

```typescript
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é :

```js
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 :

```typescript
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](https://angular.dev/essentials/signal-forms)).

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](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](https://javascript.plainenglish.io/controlvalueaccessor-is-dead-long-live-formvaluecontrol-4cf2e30a4fb0)

L’image: Photo de [Linus Sandvide](https://unsplash.com/fr/@linussandvide?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) sur [Unsplash](https://unsplash.com/fr/photos/homme-debout-tenant-une-fusee-eclairante-allumee-pendant-la-journee-RNQ-ofADZvo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
