Sitemap
ouassim ben mosbah

ouassim ben mosbah

Fullstack web developper

Angular 21.2 : Gérer la soumission des Signal Forms avec formRoot

4 min readMar 5, 2026

--

Press enter or click to view image in full size

Dans cet article, nous allons nous concentrer sur un cas d’usage précis et incontournable : la soumission d’un formulaire. Nous allons comparer l’approche manuelle (sans la directive) avec l’approche élégante (avec formRoot), tout en jetant un œil sous le capot pour comprendre comment Angular gère cela nativement.

1. L’approche manuelle : Soumettre un Signal Form sans formRoot

Même si les Signal Forms offrent un modèle de données incroyablement réactif, gérer la soumission “à la main” implique de se répéter et de gérer la plomberie habituelle du DOM.

Si l’on n’utilise pas formRoot, voici les étapes obligatoires :

  1. Ajouter manuellement l’attribut novalidate sur la balise <form> pour désactiver la validation native du navigateur
  2. Écouter l’événement natif (submit)
  3. Appeler impérativement event.preventDefault() dans le composant pour éviter le rechargement de la page
  4. Créer et maintenir ses propres Signals d’état (ex: isSubmitting) pour gérer l'interface utilisateur (comme afficher un loader ou désactiver le bouton)

Voici à quoi ressemble le code d’un formulaire de connexion basique :

<form (submit)="onSubmit($event)" novalidate>

<input type="email" [formField]="loginForm.email" />
<input type="password" [formField]="loginForm.password" />

<button type="submit" [disabled]="isSubmitting()">
@if (isSubmitting()) { Chargement... } @else { Se connecter }
</button>

</form>

Côté TypeScript :

import { Component, signal } from '@angular/core';
import { form, submit } from '@angular/forms/signals';
import { AuthService } from './auth.service';

@Component({
// ...
})
export class LoginFormComponent {
private readonly authService = inject(AuthService);

private loginModel = signal({ email: '', password: '' });

// Création du Signal Form
protected loginForm = form(this.loginModel);

// Signal d'état manuel pour l'UI
protected isSubmitting = signal(false);

async onSubmit(event: Event) {
// 1. Empêcher le comportement par défaut du navigateur
event.preventDefault();

// 2. Déclencher la validation du Signal Form
submit(this.loginForm, async () => {
// 3. Gérer l'état de chargement
this.isSubmitting.set(true);

try {
// Appel API asynchrone
await this.authService.login(this.loginForm.value());
} finally {
// 4. Rétablir l'état
this.isSubmitting.set(false);
}
});
}
}

Ce code est propre et respecte les standards actuels d’Angular. Mais avouons-le : la méthode onSubmit nous oblige toujours à mélanger la logique métier (l'appel au service) et la logique d'interface (le preventDefault, la gestion du isSubmitting). C'est un schéma répétitif que l'on va écrire pour chaque formulaire de notre application.

2. Sous le capot : Finie la magie, place au code source

Comme je vous l’avais mentionné à la fin de mon précédent article, Validation de formulaires typée dans Angular 21 : Zod et Signal Forms, il est grand temps de vous expliquer en détail la pierre angulaire de ce nouveau système : la directive formRoot.

Get ouassim ben mosbah’s stories in your inbox

Join Medium for free to get updates from this writer.

Si l’on jette un œil au code source officiel d’Angular (dans ng_signal_form.ts), voici une version simplifiée de ce que fait la directive :

import { Directive, input } from '@angular/core';
import { submit } from '../api/structure';
import { FieldTree } from '../api/types';

@Directive({
selector: 'form[formRoot]',
host: {
// 1. Désactive la validation HTML native automatiquement
'novalidate': '',
// 2. Écoute l'événement natif du DOM
'(submit)': 'onSubmit($event)',
},
})
export class FormRoot<T> {
// Le Signal Form passé en paramètre dans le template
readonly fieldTree = input.required<FieldTree<T>>({alias: 'formRoot'});

protected onSubmit(event: Event): void {
// 3. Empêche le rechargement de la page
event.preventDefault();

// 4. Déclenche la logique de soumission native du Signal Form
submit(this.fieldTree());
}
}

Comme vous pouvez le constater, c’est simplement un wrapper. Le host s'occupe de lier le DOM (novalidate, submit), et la méthode onSubmit “attrape” l'événement et déclenche la fonction submit() avec le field tree.

3. La nouvelle approche : Soumettre avec formRoot

Maintenant que nous savons comment elle fonctionne, utilisons-la. En ajoutant simplement [formRoot]="loginForm" sur notre balise <form>, nous pouvons déléguer toute la gestion de l'état de soumission à la configuration de notre formulaire.

<form [formRoot]="loginForm">

<input type="email" [formField]="loginForm.controls.email" />
<input type="password" [formField]="loginForm.controls.password" />

<button type="submit" [disabled]="loginForm.submitting()">
@if (loginForm.submitting()) { Chargement... } @else { Se connecter }
</button>

</form>

Côté TypeScript :

import { Component, signal } from '@angular/core';
import { form } from '@angular/forms/signals';
import { AuthService } from './auth.service';

@Component({
// ...
})
export class LoginFormComponent {
private readonly authService = inject(AuthService);

private loginModel = signal({ email: '', password: '' });

// On définit le comportement de soumission DIRECTEMENT dans le formulaire
protected loginForm = form(this.loginModel, (schema) => {}, {
submission: {
// Exécuté automatiquement et uniquement si le formulaire est valide !
action: async (f) => {
await this.authService.login(f().value());
},
// Exécuté automatiquement si le formulaire est invalide !
onInvalid: (f, detail) => {
console.warn("Veuillez corriger les erreurs avant de soumettre.", detail.submitted().errorSummary());
}
}
});
}

C’est tout. Plus aucun signal isSubmitting à maintenir. Plus de méthode onSubmit chargée de vérifier la validité ou de gérer les erreurs d'interface. Le composant est nettoyé et se concentre uniquement sur la déclaration de ce qui doit se passer (action).

Le formulaire génère nativement des signaux pour son état de cycle de vie : loginForm.submitting() passe automatiquement à true pendant que la promesse définie dans l'action est en cours, puis repasse à false à la fin.

--

--