

Fini les prises de tête avec RxJS. Avec Angular v21, les Signal Forms réinventent la gestion des formulaires :
- plus simples,
- plus performants
- 100% typés.
Découvrez comment maîtriser cette nouvelle API révolutionnaire à travers un guide pratique en 6 étapes ( ou presque) .
Étape 1 : Création du Signal Forms de base
Notre objectif :
Créer un modèle simple avec Nom, Prénom et une case à cocher « Êtes-vous une société ? ».
Le concept : On définit un Signal, puis on utilise la fonction form() pour générer les contrôles.
interface UserProfile {
nom: string;
prenom: string;
isCompany: boolean;
}
@Component(...)
export class UserFormComponent {
// 2. Créer le signal modèle
userModel = signal<UserProfile>({
nom: '',
prenom: '',
isCompany: false
});
// 3. Générer l'arbre du formulaire
userForm = form(this.userModel);
}<form>
<div>
<label>Nom :</label>
<input type="text" [formField]="userForm.nom" />
</div>
<div>
<label>Prénom :</label>
<input type="text" [formField]="userForm.prenom" />
</div>
<div>
<label>
<input type="checkbox" [formField]="userForm.isCompany" />
Êtes-vous une société ?
</label>
</div>
</form>Étape 2 : Ajouter la Validation

Pour l’instant, notre formulaire est un peu trop naïf : il accepte tout et n’importe quoi. Il est temps de lui donner du caractère et d’ajouter des gardes-fous pour bloquer les données invalides. Mais oubliez les anciennes classes statiques comme Validators.required : avec les Signal Forms, la validation fait peau neuve et devient purement fonctionnelle
2.1 Les validateurs natifs
Ajoutons des règles :
- Nom : Requis, min 2 chars, max 50 chars.
- Prénom : Regex pour interdire les chiffres.
import { required, minLength, maxLength, pattern } from '@angular/forms/signals';
// ...
export class UserFormComponent {
userModel = signal<UserProfile>({ nom: '', prenom: '', isCompany: false });
// On passe un deuxième argument à form() : la fonction de schéma
userForm = form(this.userModel, (controls) => {
// Validation du NOM
required(controls.nom, { message: 'Le nom est requis.' });
minLength(controls.nom, 2, { message: 'Trop court (min 2).' });
maxLength(controls.nom, 50, { message: 'Trop long (max 50).' });
// Validation du PRÉNOM (Regex : lettres uniquement)
pattern(controls.prenom, /^[a-zA-ZÀ-ÿ\s-]*$/, { message: 'Pas de chiffres dans le prénom.' });
});
}Voici la liste des munitions fournies par défaut par @angular/forms/signals pour protéger vos formulaires contre les données farfelues.
| Validateur | Type de Champ | Description | Exemple d’utilisation |
required | Tous | Le garde du corps. Interdit les valeurs vides ou false (null,false, undefined, ''). | required(c.nom, { message: 'Obligatoire !' }) |
email | String | Vérifie si ça ressemble à un email (avec un @ et un point). | email(c.contact) |
minLength | String | Rejette les textes trop courts. Idéal pour forcer des mots de passe robustes. | minLength(c.password, 8) |
maxLength | String | Coupe la chique aux bavards. Limite le nombre de caractères. | maxLength(c.bio, 280) |
pattern | String | Le shérif strict. Valide le texte contre une Regex (Expression Régulière). | pattern(c.zip, /^\d{5}$/) |
min | Number | La barre d’entrée. La valeur numérique doit être ≥ à la limite. | min(c.age, 18) |
max | Number | Le plafond. La valeur numérique doit être ≤ à la limite. | max(c.qte, 100) |
C’est la cerise sur le gâteau. Les validateurs natifs (required, email) couvrent 80% des besoins, mais pour les 20% restants (règles métier spécifiques), vous devez créer vos propres fonctions.
2.2 Zoom sur les Validateurs Custom
Parfois, les règles par défaut ne suffisent pas. Imaginez que vous vouliez interdire les adresses « @gmail.com » car vous ne voulez que des clients professionnels (B2B).
Il n’existe pas de Validators.noGmail. On va donc le fabriquer !
Un validateur custom est une fonction qui utilise la primitive validator (à importer). Elle analyse la valeur et retourne une erreur (un objet) ou null.
export function noGmail(control: any, options = { message: 'Les adresses Gmail sont interdites.' }) {
// On utilise la fonction 'validator' pour injecter la logique
validator(control, (value: string) => {
// La logique pure : si ça contient gmail, on retourne une erreur
if (value && value.toLowerCase().includes('@gmail.com')) {
// L'objet erreur doit avoir une clé 'kind' (le nom de l'erreur)
return {
kind: 'noGmail',
message: options.message
};
}
return null; // Sinon, tout va bien
});
}L’Utilisation
userForm = form(this.userModel, (c) => {
required(c.email);
email(c.email);
// Notre validateur maison !
noGmail(c.email, { message: 'Pro uniquement, pas de Gmail SVP.' });
});Étape 3 : Afficher les erreurs
Avec les Signal Forms, errors() retourne directement un tableau d’objets standardisés contenant le message que vous avez défini dans le schéma (Étape 2). Exemple de ce que renvoie le signal :
[
{ "kind": "required", "message": "Le nom est requis." },
{ "kind": "minlength", "message": "Trop court (min 2)." }
]Il faut maintenant montrer ces erreurs à l’utilisateur, mais seulement s’il a touché au champ (touched()) et que celui-ci est invalide (invalid()).
<div>
<label>Nom :</label>
<input type="text" [formField]="userForm.nom" />
@if (userForm.nom().invalid()) {
<div class="error-message">
@for (error of userForm.nom().errors(); track error.kind) {
<span>{{ error.message }}</span>
}
</div>
}
</div>Étape 4 : Logique conditionnelle

Passons aux choses sérieuses : la logique dynamique. Dans notre exemple, si l’utilisateur est une Société, le champ Prénom n’a plus lieu d’être. Nous allons voir comment faire apparaître ou disparaître ce champ automatiquement, sans écrire une seule ligne de code impérative complexe
Avec les Signals, c’est trivial car userForm.isCompany().hidden() est un signal réactif.
<label>
<input type="checkbox" [formField]="userForm.isCompany" />
Êtes-vous une société ?
</label>
@if (!userForm.prenom().hidden()) {
<div>
<label>Prénom :</label>
<input type="text" [formField]="userForm.prenom" />
</div>
}userForm = form(this.userModel, (controls) => {
// On dit : "Le champ prénom est caché SI la valeur de isCompany est vraie"
hidden(controls.prenom, ({ valueOf }) => valueOf(controls.isCompany));
});🕵️♂️ Zoom Technique : Vous avez remarqué cette syntaxe { valueOf } ?
Pour comprendre
valueOf, il faut comprendre une chose essentielle : Le Timing.Quand vous écrivez ce code, le formulaire n’existe pas encore. Angular est en train de lire votre recette pour le construire. L’objet
controlsque vous manipulez, ce ne sont pas les champs réels avec des valeurs. Ce sont juste des étiquettes (des définitions).
controls.isCompany: C’est juste une réference à la valeur avec écrit « isCompany » dessus. Il ne contient pastrueoufalse.- la méthode
valueOf(...)C’est l’outil magique qui prend le badge et va chercher la vraie valeur correspondante en temps réel.
Ces signaux contrôlent comment l’utilisateur peut (ou ne peut pas) interagir avec vos champs.
| Signal | Comportement UX | Impact Validation | Gestion dans le DOM | Cas d’usage typique |
disabled() | Visible mais grisé. Impossible de cliquer ou d’écrire dedans. | Ignoré. Même s’il est vide et requis, le formulaire reste valide. | Géré automatiquement par [formField]. Ajoute l’attribut HTML disabled. | Un champ qui dépend d’un autre (ex: Code Postal désactivé tant que Pays n’est pas choisi). |
hidden() | Invisible. Le champ disparaît complètement de l’interface. | Ignoré. Le champ ne bloque pas la soumission. | Manuel. Doit être utilisé avec un @if dans le template pour retirer l’élément. | Des questions conditionnelles (ex: « Nom de la société » seulement si Pro). |
readonly() | Visible et sélectionnable (copier-coller possible), mais non modifiable. | Ignoré (considéré comme valide par défaut car l’user ne peut rien casser). | Géré automatiquement par [formField]. Ajoute l’attribut HTML readonly. | Afficher une info confirmée (ex: « Votre identifiant : user123 »). |
userForm = form(this.userModel, (c) => {
// 1. READONLY : L'email ne peut jamais être modifié
readonly(c.email);
// 2. DISABLED : Le code promo est désactivé si le total est < 50€
disabled(c.promoCode, ({ valueOf }) => valueOf(c.totalPanier) < 50);
// 3. HIDDEN : L'adresse de livraison est cachée si "Retrait magasin" est coché
hidden(c.shippingAddress, ({ valueOf }) => valueOf(c.retraitMagasin));
});Étape 5 : Manipulation Programmatique
Un formulaire n’est pas seulement fait pour être rempli par un humain. Souvent, vous aurez besoin de charger des données depuis une API, de pré-remplir des champs ou de proposer un bouton « Reset ».

Avec les Signal Forms, fini la confusion entre patchValue() et setValue(). Vous avez deux leviers d’action : Chirurgical ou Massif.
5.1 L’approche Chirurgicale (Champ par champ)
Chaque champ expose un signal value qui est Writable (inscriptible). Vous pouvez donc utiliser les méthodes standards des Signaux : .set() ou .update().
fillForDemo() {
// On récupère l'état du champ 'nom' -> on accède à son signal 'value' -> on écrit dedans.
this.userForm.nom().value.set('TechCorp');
// Ça marche aussi avec des booléens, nombres, etc.
this.userForm.isCompany().value.set(true);
}Quand l’utiliser ? Pour modifier un champ spécifique suite à une action (ex: remplir automatiquement la ville quand on tape le Code Postal).
5.2 L’approche Massive (Via le Modèle)
C’est la grande force de l’architecture form(this.userModel). Le formulaire est « branché » sur votre signal source. Si vous changez la source, le formulaire suit !
resetForm() {
// On remplace TOUT l'objet d'un coup.
// Le formulaire va détecter le changement et mettre à jour tous les inputs.
this.userModel.set({
nom: '',
prenom: '',
isCompany: false
});
// Astuce : Pour "nettoyer" aussi les statuts (remettre touched à false),
// pensez à appeler aussi :
this.userForm.reset();
}Quand l’utiliser ? Lors du chargement de la page (après un appel HTTP httpClient.get()) ou pour un bouton « Reset complet ».
Étape 6 : La Caisse à Outils du Signal Forms (FieldState)
Vous avez remarqué qu’on utilise souvent userForm.nom() comme une fonction ? En réalité, cela retourne un objet FieldState. C’est le tableau de bord complet de votre champ.
Ces propriétés sont des Signaux. Vous devez les appeler avec
()pour lire leur valeur.
Voici les méthodes et signaux les plus importants à connaître.
| Signal | Description | Le petit détail qui tue |
value | La valeur actuelle du champ (Writable). | C’est un WritableSignal. Vous pouvez faire .set('Toto') dessus ! |
valid | true si tout est vert. | Attention : valid() n’est pas le contraire strict de invalid(). Si une validation asynchrone est en cours (pending), le champ n’est ni valide, ni invalide. Il est « en attente ». |
invalid | true si au moins une règle échoue. | Idéal pour désactiver le bouton Submit : [disabled]="form.invalid()". |
errors | La liste des erreurs. | Retourne un tableau [{ kind: 'required', ... }]. Plus besoin de deviner les clés ! |
touched | true si l’utilisateur a focus puis quitté le champ. | Utile pour ne pas agresser l’utilisateur avec des erreurs avant qu’il ait commencé à taper. |
dirty | true si la valeur a été modifiée par l’utilisateur. | Différent de touched. Si je tape « A » puis efface « A », c’est dirty mais la valeur est vide. |
pending | true si une validation async tourne. | Affichez un petit spinner pendant ce temps ! ⏳ |
submitting | true pendant la soumission. | Pratique pour bloquer les doubles clics sur le bouton Envoyer. |
Conclusion : La route est encore longue, Padawan

Ta formation ne fait que commencer. Tu maîtrises désormais les bases du sabre-laser, mais le chemin vers le rang de Maître est encore long.
D’autres défis t’attendent dans la galaxie Angular : la validation asynchrone, les listes dynamiques et les composants personnalisés. Ne relâche pas ton attention.
Que la Force (du Signal) soit avec toi.
🚀 Aller plus loin : Un bon formulaire doit savoir s’adapter à son utilisateur, y compris sa langue. 👉 [Découvrez les Meilleures Bibliothèques Angular pour l’Internationalisation]
