Après les bases de Signal Forms, voici les patterns que vous utilisez dans les vrais formulaires : listes dynamiques, validation asynchrone, champs conditionnels, schémas réutilisables et validation inter-champs. Tout s’exprime dans le schéma, pas dans le template : le template reste simple.
Listes dynamiques : applyEach
interface Invoice {
client: string;
lines: { description: string; qty: number; price: number }[];
}
readonly model = signal<Invoice>({ client: '', lines: [{ description: '', qty: 1, price: 0 }] });
readonly f = form(this.model, (p) => {
required(p.client);
applyEach(p.lines, (line) => { // les mêmes règles pour chaque élément
required(line.description);
min(line.qty, 1);
});
});
// On ajoute / retire des éléments en modifiant le MODEL : l’arbre de champs se met à jour tout seul
addLine() {
this.model.update((m) => ({ ...m, lines: [...m.lines, { description: '', qty: 1, price: 0 }] }));
}| Description | Qté | Prix | Total | |
|---|---|---|---|---|
| 3.500 RON |
Total de la facture : 3.500 RONvalide
Validation asynchrone : validateHttp / validateAsync
import { validateHttp, validateAsync } from '@angular/forms/signals';
form(this.model, (p) => {
required(p.username);
minLength(p.username, 3);
// validateHttp : une requête HTTP (via HttpClient → passe par les interceptors)
validateHttp(p.username, {
request: ({ value }) => `/api/username-available?username=${value()}`,
debounce: 400, // v22
onSuccess: (res: { available: boolean }) =>
res.available ? undefined : { kind: 'taken', message: 'Nom d’utilisateur déjà pris' },
onError: () => ({ kind: 'network', message: 'Vérification impossible' }),
});
// validateAsync : n’importe quelle resource (p. ex. un SDK, IndexedDB)
validateAsync(p.iban, {
params: ({ value }) => value(),
factory: (iban) => resource({ params: iban, loader: ({ params }) => this.bank.check(params) }),
onSuccess: (ok) => (ok ? undefined : { kind: 'iban', message: 'IBAN invalide' }),
onError: () => undefined,
});
});
// Dans le template : f.username().pending() vaut true pendant la vérification
// v22 : f.username().reloadValidation() : relance manuellement la validation asynchrone pending: false · valid: false
Champs conditionnels : hidden, disabled, readonly
form(this.model, (p) => {
// hidden : le champ n’est ni affiché NI validé
hidden(p.company, ({ valueOf }) => valueOf(p.type) !== 'company');
required(p.company);
// disabled : affiché, non modifiable, non validé (avec une raison facultative)
disabled(p.coupon, ({ valueOf }) => !valueOf(p.hasCoupon));
disabled(p.email, () => this.user.isLocked() && 'Compte verrouillé'); // une string = la raison
// readonly : affiché, non modifiable, mais sa valeur est envoyée
readonly(p.id);
// required conditionnel
required(p.phone, { when: ({ valueOf }) => valueOf(p.contactBy) === 'phone' });
});
// template : @if (!f.company().hidden()) { <input [formField]="f.company" /> }Schémas réutilisables : schema, apply, applyWhen
// Un schéma réutilisable : vous le définissez une fois et l’appliquez partout
export const addressSchema = schema<Address>((a) => {
required(a.street);
required(a.city);
pattern(a.zip, /^\d{6}$/, { message: 'Code postal : 6 chiffres' });
});
form(this.model, (p) => {
apply(p.billing, addressSchema); // toujours
applyWhen(p.shipping, ({ valueOf }) => !valueOf(p.sameAsBilling), addressSchema); // conditionnel
hidden(p.shipping, ({ valueOf }) => valueOf(p.sameAsBilling));
});Validation inter-champs
// Validation d’un champ qui dépend d’un autre
validate(p.endDate, ({ value, valueOf }) =>
value() < valueOf(p.startDate) ? { kind: 'range', message: 'La date de fin précède le début' } : undefined,
);
// validateTree : une règle au niveau du groupe qui peut signaler des erreurs sur plusieurs champs
validateTree(p, ({ value, fieldTreeOf }) => {
const { min, max } = value();
return min > max
? [
{ kind: 'range', message: 'Le minimum est supérieur', fieldTree: fieldTreeOf(p.min) },
{ kind: 'range', message: 'Le maximum est inférieur', fieldTree: fieldTreeOf(p.max) },
]
: undefined;
});Schémas Zod / Valibot
// Vous avez déjà des schémas Zod / Valibot / ArkType ? Utilisez-les directement (Standard Schema)
import { validateStandardSchema } from '@angular/forms/signals';
import { z } from 'zod';
const UserSchema = z.object({ name: z.string().min(2), email: z.email() });
form(this.model, (p) => {
validateStandardSchema(p, UserSchema); // les erreurs atterrissent sur les champs correspondants
});Pièges fréquents
- De la logique dans le template (
@if (model().type === 'company')autour des champs) sanshidden()dans le schéma → le champ masqué est quand même validé et le formulaire reste invalide. Mettez la règle dans le schéma et lisezhidden(). - Validation asynchrone sans debounce → une requête à chaque frappe.
- Listes modifiées par mutation (
model().lines.push()) → l’arbre ne se met pas à jour. Utilisezmodel.updateavec un nouveau tableau. - Erreurs sur les groupes : une erreur sur
p(la racine) n’apparaît pas à côté d’un input. Ciblez le champ avecfieldTree(ou affichezerrorSummary()).