Reactive Forms werden in v22 weiterhin vollständig unterstützt und sind nach wie vor die am häufigsten genutzten Formulare in bestehenden Anwendungen. Seit v14 sind sie typisiert. Hast du ein Projekt mit Reactive Forms, musst du nicht alles migrieren — Angular 22 bietet Brücken (SignalFormControl, compatForm), um beide Welten zu kombinieren.
Typisierte Formulare
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
// Die Typen werden aus den Startwerten abgeleitet
const profile = new FormGroup({
name: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
age: new FormControl<number | null>(null),
});
profile.value; // Partial<{ name: string; age: number | null }> — ohne die deaktivierten Controls
profile.getRawValue(); // { name: string; age: number | null } — mit allem
profile.controls.name.setValue(42); // ✗ Compilerfehler
// FormBuilder.nonNullable: jedes Control springt bei reset() auf seinen Startwert zurück
private readonly fb = inject(FormBuilder).nonNullable;
readonly form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
skills: this.fb.array(['Angular']),
});getRawValue(): {
"name": "",
"email": "",
"security": {
"password": "",
"confirm": ""
},
"skills": [
"Angular",
"TypeScript"
]
}Validatoren
// Eigener Validator (reine Funktion)
export const noWhitespace: ValidatorFn = (c) => (c.value?.trim() ? null : { whitespace: true });
// Gruppen-Validator (feldübergreifend)
export const matchingPasswords: ValidatorFn = (group) =>
group.get('password')?.value !== group.get('confirm')?.value ? { mismatch: true } : null;
this.fb.group({ password: [''], confirm: [''] }, { validators: matchingPasswords });
// Async-Validator
export function usernameAvailable(api: UsersApi): AsyncValidatorFn {
return (c) => timer(400).pipe(switchMap(() => api.isAvailable(c.value)), map((ok) => (ok ? null : { taken: true })));
}Reactive Forms ↔ Signals
// Reactive Forms + Signals: toSignal über Observables
readonly value = toSignal(this.form.valueChanges, { initialValue: this.form.getRawValue() });
readonly status = toSignal(this.form.statusChanges, { initialValue: this.form.status });
readonly canSave = computed(() => this.status() === 'VALID');
// ab v18: ein einziger Stream mit allen Events (value, status, touched, pristine, submit, reset)
this.form.events.pipe(takeUntilDestroyed()).subscribe((e) => {
if (e instanceof FormSubmittedEvent) this.analytics.track('submit');
});Schrittweise Migration zu Signal Forms v22
// Schrittweise Migration (v22): zwei Richtungen
// 1. Bottom-up — ein neues Feld mit Signal-Forms-Regeln in einer bestehenden FormGroup
import { SignalFormControl } from '@angular/forms/signals/compat';
const form = new FormGroup({
name: new SignalFormControl('Ana', (p) => { required(p); }),
age: new FormControl(25),
});
// template: <input [formField]="nameControl.fieldTree" /> <input formControlName="age" />
// 2. Top-down — ein Signal Form, das bestehende reaktive Controls enthält
import { compatForm } from '@angular/forms/signals/compat';
const lastName = new FormControl('Pop');
const model = signal({ first: '', last: lastName });
const nameForm = compatForm(model, (p) => { required(p.first); });
nameForm.last().value(); // 'Pop' — der Wert, nicht das FormControlDynamische Schlüssel: FormRecord
FormGroup setzt voraus, dass du die Schlüssel vorher kennst. Kommen sie vom Server (eine Liste von Berechtigungen, Einstellungen pro Modul), nimm FormRecord: Alle Werte haben denselben Typ, und Schlüssel lassen sich zur Laufzeit hinzufügen und entfernen.
// die Schlüssel kommen zur Laufzeit; alle Werte sind boolean
readonly permissions = new FormRecord<FormControl<boolean>>({});
constructor() {
for (const p of ['read', 'write', 'delete']) {
this.permissions.addControl(p, new FormControl(false, { nonNullable: true }));
}
}
// der Wert ist ein Objekt mit den hinzugefügten Schlüsseln
// this.permissions.value → { read: false, write: true, delete: false }Asynchrone Validatoren und updateOn
Ein asynchroner Validator gibt ein Observable oder ein Promise mit den Fehlern zurück. Während er läuft, hat das Control den Status PENDING, und das Formular ist weder gültig noch ungültig. Setze updateOn: 'blur', damit nicht bei jedem Tastendruck ein Request rausgeht.
export function usernameFree(api: UsersApi): AsyncValidatorFn {
return (control) =>
api.exists(control.value).pipe(
map((taken) => (taken ? { taken: true } : null)),
catchError(() => of(null)), // Server ausgefallen? das Formular nicht blockieren
);
}
readonly username = new FormControl('', {
nonNullable: true,
validators: [Validators.required],
asyncValidators: [usernameFree(inject(UsersApi))],
updateOn: 'blur', // beim Verlassen des Felds validieren, nicht bei jedem Tastendruck
});
// template: @if (username.pending) { wird geprüft… }Ein Stream für alles: events
Statt valueChanges, statusChanges und eigene Signals für touched oder Submit zu kombinieren, abonniere events. Du bekommst typisierte Events für Wert, Status, touched, pristine, Submit und Reset. Unterscheide sie mit instanceof.
import { FormSubmittedEvent, StatusChangeEvent, TouchedChangeEvent, ValueChangeEvent } from '@angular/forms';
this.form.events.pipe(takeUntilDestroyed()).subscribe((e) => {
if (e instanceof ValueChangeEvent) this.saveDraft(e.value);
if (e instanceof StatusChangeEvent) this.status.set(e.status);
if (e instanceof TouchedChangeEvent) this.touched.set(e.touched);
if (e instanceof FormSubmittedEvent) this.track('submit'); // und FormResetEvent, PristineChangeEvent
});Reactive Forms oder Signal Forms?
- Bestehendes Projekt mit funktionierenden Formularen — schreib sie nicht ohne Grund um.
- UI-Bibliotheken, die nur
formControlName/ CVA unterstützen. - Komplexe Abläufe auf Basis von
valueChangesund RxJS-Operatoren.
- Neue Formulare in v22.
- Viel bedingte Logik (hidden / disabled / required, wenn…).
- Das Modell soll ein einfaches Signal sein, ohne
patchValue.
Der neue Weg ist Signal Forms — die Grundlagen; ein eigenes Control, das in beiden Welten funktioniert, zeigt Eigene Formular-Controls.
Häufige Stolperfallen
form.valueschließt deaktivierte Controls aus — nimmgetRawValue(), wenn du Daten an den Server schickst.- Ohne
nonNullablesetztreset()den Wertnull→ der Typ wird zustring | null. - Abonnements auf
valueChangesohne Cleanup → ein Leak. VerwendetoSignalodertakeUntilDestroyed. - Template-driven (ngModel) + Reactive am selben Element → Konflikte. Entscheide dich für eines.
Spickzettel
new FormControl('', { nonNullable: true }) // reset() kehrt zum Anfangswert zurück, nicht zu null
new FormGroup({ name: ..., email: ... }) // feste Schlüssel
new FormArray<FormControl<string>>([]) // eine Liste von Controls
new FormRecord<FormControl<boolean>>({}) // dynamische Schlüssel, ein Typ
form.getRawValue() // enthält auch deaktivierte Controls
control.setValue(v) / patchValue(partial) / reset()
control.pending control.dirty control.touched
asyncValidators: [...] updateOn: 'blur' // Server-Validierung, bei blur
form.events // Wert, Status, touched, pristine, Submit, Reset
toSignal(control.valueChanges, { initialValue: control.value })