Reactive Forms rămân complet suportate în v22 și sunt încă cele mai folosite formulare din aplicațiile existente. Din v14 sunt tipizate. Dacă ai un proiect cu Reactive Forms, nu trebuie să migrezi tot — Angular 22 are punți (SignalFormControl, compatForm) pentru a combina cele două lumi.
Formulare tipizate
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
// Tipurile sunt deduse din valorile inițiale
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 }> — fără controalele disabled
profile.getRawValue(); // { name: string; age: number | null } — cu tot
profile.controls.name.setValue(42); // ✗ eroare de compilare
// FormBuilder.nonNullable: toate controalele revin la valoarea inițială la reset()
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"
]
}Validatori
// Validator custom (funcție pură)
export const noWhitespace: ValidatorFn = (c) => (c.value?.trim() ? null : { whitespace: true });
// Validator de grup (cross-field)
export const matchingPasswords: ValidatorFn = (group) =>
group.get('password')?.value !== group.get('confirm')?.value ? { mismatch: true } : null;
this.fb.group({ password: [''], confirm: [''] }, { validators: matchingPasswords });
// Validator async
export function usernameAvailable(api: UsersApi): AsyncValidatorFn {
return (c) => timer(400).pipe(switchMap(() => api.isAvailable(c.value)), map((ok) => (ok ? null : { taken: true })));
}Reactive Forms ↔ semnale
// Reactive Forms + semnale: toSignal peste Observable-uri
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');
// v18+: un singur stream cu toate evenimentele (value, status, touched, pristine, submit, reset)
this.form.events.pipe(takeUntilDestroyed()).subscribe((e) => {
if (e instanceof FormSubmittedEvent) this.analytics.track('submit');
});Migrare treptată spre Signal Forms v22
// Migrare treptată (v22): două direcții
// 1. Bottom-up — un câmp nou cu reguli Signal Forms într-un FormGroup existent
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 — formular semnal care conține controale reactive existente
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' — valoarea, nu FormControl-ulChei dinamice: FormRecord
FormGroup presupune că știi cheile dinainte. Când ele vin de la server (o listă de permisiuni, setări per modul), folosește FormRecord: toate valorile au același tip, iar cheile pot fi adăugate și scoase la runtime.
// cheile vin la runtime; toate valorile sunt boolean
readonly permissions = new FormRecord<FormControl<boolean>>({});
constructor() {
for (const p of ['read', 'write', 'delete']) {
this.permissions.addControl(p, new FormControl(false, { nonNullable: true }));
}
}
// valoarea e un obiect cu cheile adăugate
// this.permissions.value → { read: false, write: true, delete: false }Validatori asincroni și updateOn
Un validator asincron întoarce un Observable sau un Promise cu erorile. Cât timp rulează, controlul are statusul PENDING, iar formularul nu e nici valid, nici invalid. Pune updateOn: 'blur' ca să nu pleci cu un request la fiecare tastă.
export function usernameFree(api: UsersApi): AsyncValidatorFn {
return (control) =>
api.exists(control.value).pipe(
map((taken) => (taken ? { taken: true } : null)),
catchError(() => of(null)), // serverul a căzut? nu bloca formularul
);
}
readonly username = new FormControl('', {
nonNullable: true,
validators: [Validators.required],
asyncValidators: [usernameFree(inject(UsersApi))],
updateOn: 'blur', // validează la ieșirea din câmp, nu la fiecare tastă
});
// template: @if (username.pending) { se verifică… }Un singur flux pentru tot: events
În loc să combini valueChanges, statusChanges și propriile tale semnale pentru touched sau submit, abonează-te la events. Primești evenimente tipizate pentru valoare, status, touched, pristine, submit și reset. Le deosebești cu 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'); // și FormResetEvent, PristineChangeEvent
});Reactive sau Signal Forms?
- Proiect existent, formulare care merg — nu le rescrie fără motiv.
- Librării de UI care suportă doar
formControlName/ CVA. - Fluxuri complexe bazate pe
valueChangesși operatori RxJS.
- Formulare noi în v22.
- Logică condiționată multă (hidden / disabled / required când...).
- Vrei ca modelul să fie un semnal simplu, fără
patchValue.
Varianta nouă e Signal Forms — bazele; un control propriu care merge în ambele lumi e la Controale custom de formular.
Capcane frecvente
form.valueexclude controalele disabled — foloseștegetRawValue()când trimiți la server.- Fără
nonNullable,reset()punenull→ tipul devinestring | null. - Abonări la
valueChangesfără cleanup → leak. FoloseștetoSignalsautakeUntilDestroyed. - Template-driven (ngModel) + Reactive pe același element → conflicte. Alege unul.
Fișă rapidă
new FormControl('', { nonNullable: true }) // reset() revine la valoarea inițială, nu la null
new FormGroup({ name: ..., email: ... }) // chei fixe
new FormArray<FormControl<string>>([]) // listă de controale
new FormRecord<FormControl<boolean>>({}) // chei dinamice, același tip
form.getRawValue() // include și controalele disabled
control.setValue(v) / patchValue(partial) / reset()
control.pending control.dirty control.touched
asyncValidators: [...] updateOn: 'blur' // validare la server, la blur
form.events // value, status, touched, pristine, submit, reset
toSignal(control.valueChanges, { initialValue: control.value })