Nach den Grundlagen von Signal Forms kommen hier die Muster, die du in echten Formularen brauchst: dynamische Listen, asynchrone Validierung, bedingte Felder, wiederverwendbare Schemas und feldübergreifende Validierung. Sie werden alle im Schema ausgedrückt, nicht im Template — das Template bleibt einfach.
Dynamische Listen: 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) => { // dieselben Regeln für jedes Element
required(line.description);
min(line.qty, 1);
});
});
// Elemente fügst du hinzu oder entfernst du, indem du das MODEL änderst — der Feldbaum aktualisiert sich von selbst
addLine() {
this.model.update((m) => ({ ...m, lines: [...m.lines, { description: '', qty: 1, price: 0 }] }));
}| Beschreibung | Menge | Preis | Total | |
|---|---|---|---|---|
| 3.500 RON |
Rechnungssumme: 3.500 RONgültig
Asynchrone Validierung: validateHttp / validateAsync
import { validateHttp, validateAsync } from '@angular/forms/signals';
form(this.model, (p) => {
required(p.username);
minLength(p.username, 3);
// validateHttp: ein HTTP-Request (über HttpClient → läuft durch die Interceptors)
validateHttp(p.username, {
request: ({ value }) => `/api/username-available?username=${value()}`,
debounce: 400, // v22
onSuccess: (res: { available: boolean }) =>
res.available ? undefined : { kind: 'taken', message: 'Username vergeben' },
onError: () => ({ kind: 'network', message: 'Konnte nicht geprüft werden' }),
});
// validateAsync: jede beliebige Resource (z. B. ein 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: 'Ungültige IBAN' }),
onError: () => undefined,
});
});
// Im Template: f.username().pending() ist true, solange die Prüfung läuft
// v22: f.username().reloadValidation() — führt die asynchrone Validierung manuell erneut aus pending: false · valid: false
Bedingte Felder: hidden, disabled, readonly
form(this.model, (p) => {
// hidden: das Feld wird nicht angezeigt UND nicht validiert
hidden(p.company, ({ valueOf }) => valueOf(p.type) !== 'company');
required(p.company);
// disabled: angezeigt, nicht editierbar, nicht validiert (mit optionalem Grund)
disabled(p.coupon, ({ valueOf }) => !valueOf(p.hasCoupon));
disabled(p.email, () => this.user.isLocked() && 'Konto ist gesperrt'); // ein String = der Grund
// readonly: angezeigt, nicht editierbar, aber der Wert wird gesendet
readonly(p.id);
// bedingtes required
required(p.phone, { when: ({ valueOf }) => valueOf(p.contactBy) === 'phone' });
});
// template: @if (!f.company().hidden()) { <input [formField]="f.company" /> }Wiederverwendbare Schemas: schema, apply, applyWhen
// Ein wiederverwendbares Schema — du definierst es einmal und wendest es überall an
export const addressSchema = schema<Address>((a) => {
required(a.street);
required(a.city);
pattern(a.zip, /^\d{6}$/, { message: 'Postleitzahl: 6 Ziffern' });
});
form(this.model, (p) => {
apply(p.billing, addressSchema); // immer
applyWhen(p.shipping, ({ valueOf }) => !valueOf(p.sameAsBilling), addressSchema); // bedingt
hidden(p.shipping, ({ valueOf }) => valueOf(p.sameAsBilling));
});Feldübergreifende Validierung
// Validierung auf einem Feld, das von einem anderen abhängt
validate(p.endDate, ({ value, valueOf }) =>
value() < valueOf(p.startDate) ? { kind: 'range', message: 'Das Enddatum liegt vor dem Start' } : undefined,
);
// validateTree: eine Regel auf Gruppenebene, die Fehler auf mehreren Feldern melden kann
validateTree(p, ({ value, fieldTreeOf }) => {
const { min, max } = value();
return min > max
? [
{ kind: 'range', message: 'Das Minimum ist größer', fieldTree: fieldTreeOf(p.min) },
{ kind: 'range', message: 'Das Maximum ist kleiner', fieldTree: fieldTreeOf(p.max) },
]
: undefined;
});Zod- / Valibot-Schemas
// Du hast bereits Zod-/Valibot-/ArkType-Schemas? Verwende sie direkt (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); // die Fehler landen auf den passenden Feldern
});Häufige Stolperfallen
- Logik im Template (
@if (model().type === 'company')um Felder) ohnehidden()im Schema → das ausgeblendete Feld wird trotzdem validiert und das Formular bleibt ungültig. Lege die Regel ins Schema und lieshidden(). - Asynchrone Validierung ohne Debounce → bei jedem Tastendruck ein Request.
- Listen, die per Mutation verändert werden (
model().lines.push()) → der Baum aktualisiert sich nicht. Verwendemodel.updatemit einem neuen Array. - Fehler auf Gruppen: Ein Fehler auf
p(der Wurzel) erscheint nicht neben einem Input. Ziele mitfieldTreeauf das Feld (oder zeigeerrorSummary()an).