Reactive Forms e Signal Forms
Reactive Forms
ngModel (template-driven forms) è vietato. Usare sempre Reactive Forms.
Motivazioni:
- Tipizzazione forte
- Testabilità
- Validazione centralizzata
- Composabilità
Binding diretto al controllo
Passare sempre il controllo direttamente all'input con [formControl], non usare formControlName:
import { FormGroup, FormControl, Validators } from '@angular/forms';
readonly fg = new FormGroup({
name: new FormControl<string>('', { nonNullable: true, validators: Validators.required }),
email: new FormControl<string>('', { nonNullable: true, validators: [Validators.required, Validators.email] })
});
<input [formControl]="fg.controls.name" />
<input [formControl]="fg.controls.email" />
<button [disabled]="fg.invalid" (click)="onSubmit()">Invia</button>
Il tag <form> si usa solo quando serve intercettare eventi nativi del browser, ad esempio il tasto Enter per fare submit:
<form (ngSubmit)="onSubmit()">
<input [formControl]="fg.controls.name" />
<input [formControl]="fg.controls.email" />
<button type="submit" [disabled]="fg.invalid">Invia</button>
</form>
Typed Forms
Dalla v14+ i Reactive Forms sono fortemente tipizzati:
readonly fg = new FormGroup({
name: new FormControl<string>('', { nonNullable: true }),
age: new FormControl<number | null>(null)
});
// fg.value è tipizzato correttamente
const name: string = this.fg.controls.name.value;
Validazione custom sincrona
function minAge(min: number): ValidatorFn {
return (control: AbstractControl) => {
const value = control.value;
return value >= min ? null : { minAge: { required: min, actual: value } };
};
}
Validazione condizionale
Un validatore a livello di FormGroup consente di rendere un campo obbligatorio in base al valore di un altro. La logica è centralizzata nel gruppo: i singoli controlli non sanno nulla l'uno dell'altro.
const requiredIfChecked: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
const fg = group as FormGroup;
const checked = fg.controls['hasDiscount'].value as boolean;
const code = fg.controls['discountCode'].value as string;
return checked && !code ? { discountCodeRequired: true } : null;
};
readonly fg = new FormGroup(
{
hasDiscount: new FormControl<boolean>(false, { nonNullable: true }),
discountCode: new FormControl<string>('', { nonNullable: true })
},
{ validators: requiredIfChecked }
);
L'errore vive sul gruppo, non sul controllo figlio. Nel template si legge da fg.errors:
<input type="checkbox" [formControl]="fg.controls.hasDiscount" />
<input [formControl]="fg.controls.discountCode" />
@if (fg.errors?.['discountCodeRequired']) {
<span>Il codice sconto è obbligatorio</span>
}
Aggiornare dinamicamente i validator del controllo figlio (con
setValidators/updateValueAndValidity) è una soluzione alternativa ma più fragile: disperde la logica tra costruttore e lifecycle hooks. Preferire il validatore di gruppo.
Validazione asincrona
I validatori asincroni ricevono un AbstractControl e restituiscono un Observable o Promise di ValidationErrors | null:
function uniqueUsernameValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return userService.checkUsername(control.value).pipe(
map(isTaken => (isTaken ? { usernameTaken: true } : null)),
catchError(() => of(null))
);
};
}
Utilizzo con iniezione della dipendenza tramite inject:
readonly fg = new FormGroup({
username: new FormControl<string>('', {
nonNullable: true,
validators: Validators.required,
asyncValidators: uniqueUsernameValidator(inject(UserService))
})
});
Template per mostrare lo stato asincrono:
<input [formControl]="fg.controls.username" />
@if (fg.controls.username.pending) {
<span>Verifica in corso…</span>
}
@if (fg.controls.username.errors?.['usernameTaken']) {
<span>Username già in uso</span>
}
Per evitare troppe chiamate HTTP, combinare il validatore asincrono con
debounceTimeoppure usareupdateOn: 'blur'sul controllo.
Signal Forms
Le Signal-based Forms vanno preferite ai Reactive Forms classici se disponibili nella versione in uso.
⚠️ Verificare lo stato di rilascio nella propria versione di Angular. Potrebbero essere ancora in developer preview. I Reactive Forms classici restano comunque validi e non sono deprecati.
Vantaggi rispetto ai Reactive Forms:
- Integrazione nativa con il sistema di Signals
- Nessun bisogno di
valueChangesObservable - Reattività granulare automatica