După bazele din Signal Forms, aici sunt pattern-urile pe care le folosești în formulare reale: liste dinamice, validare asincronă, câmpuri condiționale, scheme reutilizabile și validări între câmpuri. Toate sunt exprimate în schemă, nu în template — template-ul rămâne simplu.
Liste dinamice: 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) => { // aceleași reguli pentru fiecare element
required(line.description);
min(line.qty, 1);
});
});
// Adaugi / ștergi elemente modificând MODELUL — arborele de câmpuri se actualizează singur
addLine() {
this.model.update((m) => ({ ...m, lines: [...m.lines, { description: '', qty: 1, price: 0 }] }));
}| Descriere | Cant. | Preț | Total | |
|---|---|---|---|---|
| 3.500 RON |
Total factură: 3.500 RONvalidă
Validare asincronă: validateHttp / validateAsync
import { validateHttp, validateAsync } from '@angular/forms/signals';
form(this.model, (p) => {
required(p.username);
minLength(p.username, 3);
// validateHttp: request HTTP (prin HttpClient → trece prin interceptori)
validateHttp(p.username, {
request: ({ value }) => `/api/username-available?username=${value()}`,
debounce: 400, // v22
onSuccess: (res: { available: boolean }) =>
res.available ? undefined : { kind: 'taken', message: 'Username ocupat' },
onError: () => ({ kind: 'network', message: 'Nu am putut verifica' }),
});
// validateAsync: orice resursă (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 invalid' }),
onError: () => undefined,
});
});
// În template: f.username().pending() e true cât timp rulează verificarea
// v22: f.username().reloadValidation() — re-rulează manual validarea async pending: false · valid: false
Câmpuri condiționale: hidden, disabled, readonly
form(this.model, (p) => {
// hidden: câmpul nu e afișat ȘI nu e validat
hidden(p.company, ({ valueOf }) => valueOf(p.type) !== 'company');
required(p.company);
// disabled: afișat, needitabil, nevalidat (cu motiv opțional)
disabled(p.coupon, ({ valueOf }) => !valueOf(p.hasCoupon));
disabled(p.email, () => this.user.isLocked() && 'Contul e blocat'); // string = motivul
// readonly: afișat, needitabil, dar valoarea e trimisă
readonly(p.id);
// required condiționat
required(p.phone, { when: ({ valueOf }) => valueOf(p.contactBy) === 'phone' });
});
// template: @if (!f.company().hidden()) { <input [formField]="f.company" /> }Scheme reutilizabile: schema, apply, applyWhen
// Schemă reutilizabilă — o definești o dată, o aplici oriunde
export const addressSchema = schema<Address>((a) => {
required(a.street);
required(a.city);
pattern(a.zip, /^\d{6}$/, { message: 'Cod poștal: 6 cifre' });
});
form(this.model, (p) => {
apply(p.billing, addressSchema); // mereu
applyWhen(p.shipping, ({ valueOf }) => !valueOf(p.sameAsBilling), addressSchema); // condiționat
hidden(p.shipping, ({ valueOf }) => valueOf(p.sameAsBilling));
});Validări între câmpuri
// Validare pe un câmp care depinde de altul
validate(p.endDate, ({ value, valueOf }) =>
value() < valueOf(p.startDate) ? { kind: 'range', message: 'Data de final e înainte de început' } : undefined,
);
// validateTree: o regulă la nivel de grup care poate raporta erori pe mai multe câmpuri
validateTree(p, ({ value, fieldTreeOf }) => {
const { min, max } = value();
return min > max
? [
{ kind: 'range', message: 'Minimul e mai mare', fieldTree: fieldTreeOf(p.min) },
{ kind: 'range', message: 'Maximul e mai mic', fieldTree: fieldTreeOf(p.max) },
]
: undefined;
});Scheme Zod / Valibot
// Ai deja scheme Zod / Valibot / ArkType? Folosește-le direct (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); // erorile ajung pe câmpurile corespunzătoare
});Capcane frecvente
- Logică în template (
@if (model().type === 'company')peste câmpuri) fărăhidden()în schemă → câmpul ascuns e tot validat și formularul rămâne invalid. Pune regula în schemă și citeștehidden(). - Validare async fără debounce → un request la fiecare tastă.
- Liste modificate prin mutație (
model().lines.push()) → arborele nu se actualizează. Foloseștemodel.updatecu array nou. - Erori pe grupuri: o eroare pe
p(rădăcină) nu apare lângă un input. Țintește câmpul cufieldTree(sau afișeazăerrorSummary()).