Signal Forms: i Form Reattivi del Futuro
I Reactive Forms di Angular, basati su FormControl, FormGroup e
FormArray, hanno rappresentato per anni lo standard per la gestione dei form
complessi. Tuttavia, con l'introduzione dei Signals come primitiva reattiva
centrale del framework, il team Angular sta lavorando a una nuova generazione di form
completamente basati su Signal: i Signal Forms. Questa evoluzione promette
di eliminare la dipendenza da RxJS per lo stato dei form, ridurre drasticamente il boilerplate
e integrare nativamente la reattività fine-grained nel sistema di gestione dei form.
In questo quinto articolo della serie Angular Moderno, analizzeremo i limiti dei Reactive Forms attuali, esploreremo la proposta RFC per i Signal Forms, vedremo come funzionano le nuove primitive e come prepararsi alla migrazione. Che tu stia iniziando un nuovo progetto o gestendo un'applicazione enterprise esistente, questa guida ti fornira le conoscenze necessarie per affrontare il futuro dei form in Angular.
Panoramica della Serie Angular Moderno
| # | Articolo | Focus |
|---|---|---|
| 1 | Angular Signals | Reattività fine-grained |
| 2 | Zoneless Change Detection | Eliminare Zone.js |
| 3 | Nuovi Template @if, @for, @defer | Control flow moderno |
| 4 | Standalone Components | Architettura senza NgModule |
| 5 | Sei qui - Signal Forms | Form reattivi con Signals |
| 6 | SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance e metriche |
| 8 | Angular PWA | Progressive Web App |
| 9 | Dependency Injection Avanzata | DI tree-shakable |
| 10 | Migrare da Angular 17 a 21 | Guida alla migrazione |
I Limiti dei Reactive Forms Attuali
I Reactive Forms sono stati introdotti con Angular 2 e si basano su una gerarchia di classi
astratte (AbstractControl, FormControl, FormGroup,
FormArray) che utilizzano RxJS Observable per notificare i cambiamenti
di valore e stato. Sebbene potenti, presentano diversi limiti strutturali che diventano evidenti
nelle applicazioni complesse.
Problemi Strutturali dei Reactive Forms
- Dipendenza da RxJS: Ogni
FormControlesponevalueChangesestatusChangescome Observable, costringendo gli sviluppatori a gestire sottoscrizioni, operatori e memory leak - Tipizzazione debole: Fino ad Angular 14,
FormGrouprestituivaany. I Typed Forms hanno migliorato la situazione, ma la sintassi resta verbosa - Gerarchia di AbstractControl: La catena di ereditarieta rende difficile estendere o personalizzare il comportamento dei controlli
- Boilerplate eccessivo: Creare un form con validazione richiede decine di righe di codice TypeScript prima ancora di toccare il template
- Stato implicito: Proprietà come
dirty,touched,validsono mutabili e non derivate, causando inconsistenze - Nessuna integrazione con Signals: I Reactive Forms non sfruttano il sistema di reattività fine-grained dei Signals, obbligando a convertire manualmente tra Observable e Signal
- Reset complesso: Il reset dello stato del form (valori, dirty, touched) richiede chiamate multiple e non sempre si comporta come previsto
// Un form di registrazione con i Reactive Forms attuali
@Component({
selector: 'app-registration',
standalone: true,
imports: [ReactiveFormsModule],
template: `...`
})
export class RegistrationComponent implements OnInit, OnDestroy {
private fb = inject(FormBuilder);
private destroy$ = new Subject<void>();
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
address: this.fb.group({
street: [''],
city: ['', Validators.required],
zip: ['', Validators.pattern(/^\d{5}$/)]
})
});
// Devo sottoscrivermi manualmente per reagire ai cambiamenti
ngOnInit() {
this.form.get('password')?.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.form.get('confirmPassword')?.updateValueAndValidity();
});
}
// Devo gestire manualmente la distruzione delle sottoscrizioni
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get nameErrors() {
const ctrl = this.form.get('name');
if (ctrl?.hasError('required')) return 'Nome obbligatorio';
if (ctrl?.hasError('minlength')) return 'Minimo 2 caratteri';
return '';
}
}
Come si può notare, anche un form moderatamente complesso richiede una quantità significativa di codice boilerplate: sottoscrizioni manuali, gestione del ciclo di vita, getter per i messaggi di errore, e conversioni tra Observable e valori imperativi. I Signal Forms promettono di risolvere tutti questi problemi.
La Proposta Signal Forms: RFC e Nuove Primitive
Il team Angular ha pubblicato un RFC (Request for Comments) per i Signal Forms, una riscrittura completa del sistema di gestione dei form basata sui Signals. L'obiettivo principale è creare un'API che sia reattiva per natura, senza dipendere da RxJS per lo stato del form, mantenendo al contempo la potenza e la flessibilità dei Reactive Forms attuali.
Le Nuove Primitive Proposte
| Primitiva Signal Forms | Equivalente Reactive Forms | Descrizione |
|---|---|---|
FormSignal |
FormControl |
Un singolo valore del form, writable signal con stato reattivo |
FormRecord |
FormGroup |
Gruppo di signal form, compone più FormSignal in un oggetto |
FormList |
FormArray |
Lista dinamica di signal form per collezioni ripetute |
computed() |
valueChanges |
Derivazione reattiva automatica dal valore del form |
effect() |
.subscribe() |
Side-effect automatico con cleanup senza gestione manuale |
// Lo stesso form di registrazione con Signal Forms
@Component({
selector: 'app-registration',
standalone: true,
template: `...`
})
export class RegistrationComponent {
// Dichiarazione concisa con validazione inline
name = formSignal('', {
validators: [required(), minLength(2)]
});
email = formSignal('', {
validators: [required(), email()]
});
password = formSignal('', {
validators: [required(), minLength(8)]
});
confirmPassword = formSignal('', {
validators: [required(), matchesField(this.password)]
});
address = formRecord({
street: formSignal(''),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [pattern(/^\d{5}$/)] })
});
// Lo stato è derivato automaticamente, nessuna sottoscrizione
isFormValid = computed(() =>
this.name.isValid() &&
this.email.isValid() &&
this.password.isValid() &&
this.confirmPassword.isValid() &&
this.address.isValid()
);
// Nessun ngOnInit, nessun ngOnDestroy, nessun Subject
}
Vantaggi Chiave dei Signal Forms
- Zero sottoscrizioni: Nessun
subscribe(), nessuntakeUntil, nessun memory leak da gestire - Stato derivato: Proprietà come
isValid(),isDirty(),errors()sono computed signal, sempre sincronizzati - Type-safe nativamente: Il tipo del valore è inferito automaticamente dal valore iniziale
- Integrazione con il template: I signal si collegano direttamente al template senza pipe o conversioni
- Cleanup automatico: Nessuna gestione manuale del ciclo di vita del componente
Stato del Form Basato su Signals
Uno dei cambiamenti più significativi dei Signal Forms e che tutto lo stato del form
diventa un Signal. Non ci sono più proprietà mutabili come .dirty o
.touched che vengono aggiornate internamente: ogni aspetto dello stato e un
computed signal derivato dalla sorgente di verita.
const username = formSignal('', {
validators: [required(), minLength(3), maxLength(20)]
});
// Il valore corrente: WritableSignal<string>
console.log(username.value()); // ''
// Stato di validazione: Signal<boolean>
console.log(username.isValid()); // false
console.log(username.isInvalid()); // true
// Errori di validazione: Signal<ValidationErrors | null>
console.log(username.errors()); // { required: true }
// Stato di interazione: Signal<boolean>
console.log(username.isDirty()); // false
console.log(username.isPristine()); // true
console.log(username.isTouched()); // false
console.log(username.isUntouched()); // true
// Stato di attesa (validazione asincrona): Signal<boolean>
console.log(username.isPending()); // false
// Stato di disabilitazione: Signal<boolean>
console.log(username.isDisabled()); // false
console.log(username.isEnabled()); // true
// Aggiornamento programmatico del valore
username.value.set('angular_dev');
// Ora: isValid() === true, isDirty() === true, errors() === null
// Marcare come touched (ad esempio on blur)
username.markAsTouched();
// Ora: isTouched() === true
Confronto: Stato nei Reactive Forms vs Signal Forms
| Proprietà | Reactive Forms | Signal Forms |
|---|---|---|
| Valore | control.value (getter mutabile) |
control.value() (Signal) |
| Validita | control.valid (proprietà booleana) |
control.isValid() (computed Signal) |
| Errori | control.errors (oggetto o null) |
control.errors() (Signal) |
| Dirty | control.dirty (booleano mutato internamente) |
control.isDirty() (computed Signal) |
| Osservare cambiamenti | control.valueChanges.subscribe() |
effect(() => console.log(control.value())) |
| Cleanup | Manuale (unsubscribe, takeUntil) | Automatico (legato al DestroyRef) |
Il vantaggio fondamentale è che lo stato è sempre coerente. In un Reactive Form,
potresti osservare uno stato intermedio dove dirty è aggiornato ma errors
non lo è ancora. Con i Signal Forms, tutti i computed si aggiornano atomicamente nel glitch-free
scheduling dei Signals.
Validazione con Signals
I Signal Forms reimpostano completamente il sistema di validazione. I validatori diventano
funzioni pure che restituiscono Signal di errori, con supporto nativo per
validazione sincrona e asincrona senza bisogno di interfacce separate come
Validator e AsyncValidator.
// Validatori built-in
const email = formSignal('', {
validators: [
required(), // Non può essere vuoto
email(), // Deve essere un'email valida
maxLength(100) // Massimo 100 caratteri
]
});
// Validatore personalizzato: funzione pura
function strongPassword(): ValidatorFn {
return (value: string) => {
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasDigit = /\d/.test(value);
const hasSpecial = /[!@#$%^&*]/.test(value);
if (hasUpperCase && hasLowerCase && hasDigit && hasSpecial) {
return null; // Valido
}
return {
strongPassword: {
hasUpperCase,
hasLowerCase,
hasDigit,
hasSpecial,
message: 'La password deve contenere maiuscole, minuscole, numeri e caratteri speciali'
}
};
};
}
const password = formSignal('', {
validators: [required(), minLength(8), strongPassword()]
});
// Accesso reattivo agli errori specifici
const passwordErrors = computed(() => {
const errs = password.errors();
if (!errs) return [];
const messages: string[] = [];
if (errs['required']) messages.push('Password obbligatoria');
if (errs['minlength']) messages.push('Minimo 8 caratteri');
if (errs['strongPassword']) messages.push(errs['strongPassword'].message);
return messages;
});
// Validatore asincrono: controlla se l'username è disponibile
function usernameAvailable(userService: UserService): AsyncValidatorFn {
return async (value: string) => {
if (value.length < 3) return null; // Troppo corto, saltiamo la verifica
const exists = await userService.checkUsername(value);
return exists
? { usernameTaken: { message: `L'username "${value}" è già in uso` } }
: null;
};
}
@Component({
selector: 'app-signup',
standalone: true,
template: `
<label>Username</label>
<input [formSignal]="username" />
@if (username.isPending()) {
<span class="checking">Verifica in corso...</span>
}
@if (username.errors()?.['usernameTaken']; as err) {
<span class="error">{{ err.message }}</span>
}
`
})
export class SignupComponent {
private userService = inject(UserService);
username = formSignal('', {
validators: [required(), minLength(3)],
asyncValidators: [usernameAvailable(this.userService)],
asyncDebounceMs: 300 // Debounce integrato!
});
}
Novità nella Validazione con Signal Forms
- Debounce integrato: I validatori asincroni supportano
asyncDebounceMsnativamente, senza bisogno didebounceTime()di RxJS - Stato pending reattivo:
isPending()e un Signal che si aggiorna automaticamente durante la validazione asincrona - Validatori cross-field: Possono accedere ad altri FormSignal come dependency, con aggiornamento automatico quando i valori collegati cambiano
- Composizione: I validatori sono semplici funzioni, facilmente componibili e testabili
Form Groups con Signals: Composizione e Annidamento
formRecord() permette di comporre più formSignal in una struttura
gerarchica, equivalente al FormGroup dei Reactive Forms. La differenza chiave
è che lo stato del record è automaticamente derivato dallo stato dei suoi figli.
// Form completo con gruppi annidati
const profileForm = formRecord({
personalInfo: formRecord({
firstName: formSignal('', { validators: [required()] }),
lastName: formSignal('', { validators: [required()] }),
birthDate: formSignal<Date | null>(null)
}),
contactInfo: formRecord({
email: formSignal('', { validators: [required(), email()] }),
phone: formSignal('', { validators: [pattern(/^\+?\d{10,15}$/)] })
}),
preferences: formRecord({
newsletter: formSignal(false),
theme: formSignal<'light' | 'dark'>('dark'),
language: formSignal('it')
}),
// FormList per indirizzi multipli
addresses: formList([
formRecord({
type: formSignal<'home' | 'work'>('home'),
street: formSignal('', { validators: [required()] }),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [required()] })
})
])
});
// Lo stato si propaga automaticamente verso l'alto
const isProfileValid = profileForm.isValid();
// true solo se TUTTI i campi di TUTTI i gruppi sono validi
const isDirty = profileForm.isDirty();
// true se QUALSIASI campo in qualsiasi gruppo e stato modificato
// Accesso al valore completo come oggetto tipizzato
const formValue = profileForm.value();
// Tipo inferito automaticamente:
// {
// personalInfo: { firstName: string; lastName: string; birthDate: Date | null }
// contactInfo: { email: string; phone: string }
// preferences: { newsletter: boolean; theme: 'light' | 'dark'; language: string }
// addresses: Array<{ type: 'home' | 'work'; street: string; city: string; zip: string }>
// }
// Aggiungere un indirizzo alla lista
profileForm.controls.addresses.push(
formRecord({
type: formSignal<'home' | 'work'>('work'),
street: formSignal('', { validators: [required()] }),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [required()] })
})
);
Propagazione Automatica dello Stato
A differenza dei Reactive Forms dove lo stato del gruppo viene calcolato internamente
e può causare inconsistenze temporanee, nei Signal Forms lo stato di un formRecord
è un computed signal che dipende dai suoi figli. Questo significa che:
formRecord.isValid()ècomputed(() => children.every(c => c.isValid()))formRecord.isDirty()ècomputed(() => children.some(c => c.isDirty()))- Non esistono stati intermedi inconsistenti grazie al glitch-free scheduling
- Il reset di un record resetta tutti i figli atomicamente
Two-Way Binding con model() e Signal Forms
Angular ha introdotto la funzione model() come un two-way bindable signal
per i componenti. I Signal Forms si integrano perfettamente con model(), creando
un flusso di dati bidirezionale pulito tra il template e lo stato del form senza bisogno
di direttive speciali come formControlName.
// Componente input personalizzato con model()
@Component({
selector: 'app-text-input',
standalone: true,
template: `
<div class="input-wrapper" [class.invalid]="hasError()">
<label>{{ label() }}</label>
<input
[value]="value()"
(input)="onInput($event)"
(blur)="onBlur()"
[placeholder]="placeholder()"
/>
@if (hasError() && isTouched()) {
<span class="error-message">{{ errorMessage() }}</span>
}
</div>
`
})
export class TextInputComponent {
// Two-way binding con il componente padre
value = model<string>('');
label = input<string>('');
placeholder = input<string>('');
hasError = input<boolean>(false);
isTouched = input<boolean>(false);
errorMessage = input<string>('');
onInput(event: Event) {
const target = event.target as HTMLInputElement;
this.value.set(target.value);
}
onBlur() {
// Notifica il padre che il campo e stato toccato
}
}
// Utilizzo nel componente padre con Signal Forms
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [TextInputComponent],
template: `
<form (ngSubmit)="submit()">
<app-text-input
[(value)]="name.value"
[label]="'Nome completo'"
[hasError]="name.isInvalid()"
[isTouched]="name.isTouched()"
[errorMessage]="nameError()"
/>
<app-text-input
[(value)]="email.value"
[label]="'Email'"
[hasError]="email.isInvalid()"
[isTouched]="email.isTouched()"
[errorMessage]="emailError()"
/>
<button [disabled]="!isFormValid()">Invia</button>
</form>
`
})
export class ContactFormComponent {
name = formSignal('', { validators: [required()] });
email = formSignal('', { validators: [required(), email()] });
nameError = computed(() => {
if (this.name.errors()?.['required']) return 'Nome obbligatorio';
return '';
});
emailError = computed(() => {
if (this.email.errors()?.['required']) return 'Email obbligatoria';
if (this.email.errors()?.['email']) return 'Email non valida';
return '';
});
isFormValid = computed(() =>
this.name.isValid() && this.email.isValid()
);
submit() {
console.log({ name: this.name.value(), email: this.email.value() });
}
}
Vantaggi dell'Integrazione model() + Signal Forms
- Nessuna direttiva speciale: Non servono
[formControl]oformControlNamenel template - Componenti riutilizzabili: Gli input personalizzati ricevono e restituiscono valori tramite
model(), completamente indipendenti dalla logica del form - Type-safety completa: Il tipo del valore scorre dal FormSignal attraverso model() fino al template
- Testabilità: I componenti input possono essere testati isolatamente, senza FormModule
Pattern Pratici Oggi: Approccio Ibrido Signals + Reactive Forms
Mentre i Signal Forms sono ancora in fase di RFC, è possibile iniziare a sfruttare i Signals
con i Reactive Forms attuali, adottando un approccio ibrido che facilità
la futura migrazione. Questo significa usare toSignal() per convertire gli
Observable dei form in Signals e costruire lo stato derivato con computed().
@Component({
selector: 'app-search',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="searchControl" placeholder="Cerca..." />
@if (isSearching()) {
<span class="spinner">Ricerca in corso...</span>
}
@if (hasResults()) {
<p>Trovati {{ resultCount() }} risultati per "{{ searchTerm() }}"</p>
}
@for (result of results(); track result.id) {
<app-result-card [result]="result" />
} @empty {
@if (searchTerm().length > 0) {
<p>Nessun risultato per "{{ searchTerm() }}"</p>
}
}
`
})
export class SearchComponent {
private searchService = inject(SearchService);
searchControl = new FormControl('', { nonNullable: true });
// Converto valueChanges in Signal
searchTerm = toSignal(
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
),
{ initialValue: '' }
);
// Stato derivato con computed
private searchResult = toSignal(
toObservable(this.searchTerm).pipe(
switchMap(term =>
term.length > 2
? this.searchService.search(term)
: of({ items: [], total: 0 })
)
),
{ initialValue: { items: [], total: 0 } }
);
results = computed(() => this.searchResult().items);
resultCount = computed(() => this.searchResult().total);
hasResults = computed(() => this.resultCount() > 0);
isSearching = signal(false);
}
Strategia di Transizione: Da Reactive Forms a Signal Forms
- Fase 1 (oggi): Converti
valueChangesin Signal contoSignal(). Usacomputed()per stato derivato - Fase 2 (oggi): Isola la logica del form in service dedicati che espongono Signal
- Fase 3 (quando disponibile): Sostituisci i FormControl con FormSignal uno alla volta
- Fase 4: Rimuovi
ReactiveFormsModulee le dipendenze RxJS relative ai form
Custom Form Controls con Signals
L'interfaccia ControlValueAccessor (CVA) è il meccanismo attuale per creare
componenti form personalizzati. Con i Signal Forms, questo pattern verrà semplificato
drasticamente. Tuttavia, già oggi è possibile costruire CVA più puliti sfruttando i Signals.
// CVA attuale migliorato con Signals
@Component({
selector: 'app-star-rating',
standalone: true,
template: `
<div class="star-rating" [class.disabled]="isDisabled()">
@for (star of stars(); track star) {
<button
class="star"
[class.filled]="star <= currentValue()"
[class.hovered]="star <= hoveredStar()"
(mouseenter)="hoveredStar.set(star)"
(mouseleave)="hoveredStar.set(0)"
(click)="selectRating(star)"
[disabled]="isDisabled()">
★
</button>
}
<span class="label">{{ currentValue() }} / {{ maxStars() }}</span>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true
}
]
})
export class StarRatingComponent implements ControlValueAccessor {
maxStars = input(5);
currentValue = signal(0);
hoveredStar = signal(0);
isDisabled = signal(false);
stars = computed(() =>
Array.from({ length: this.maxStars() }, (_, i) => i + 1)
);
private onChange: (value: number) => void = () => {};
private onTouched: () => void = () => {};
writeValue(value: number): void {
this.currentValue.set(value || 0);
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.isDisabled.set(isDisabled);
}
selectRating(star: number): void {
if (this.isDisabled()) return;
this.currentValue.set(star);
this.onChange(star);
this.onTouched();
}
}
CVA Oggi vs Signal Forms Futuro
| Aspetto | CVA Attuale | Signal Forms (futuro) |
|---|---|---|
| Interfaccia da implementare | ControlValueAccessor (4 metodi) |
Nessuna interfaccia, solo model() |
| Registrazione | NG_VALUE_ACCESSOR provider |
Automatica tramite il binding [(value)] |
| Callback onChange/onTouched | Manuale, imperativo | Automatico, reattivo via Signal |
| Stato disabled | setDisabledState() imperativo |
Signal isDisabled() dal parent |
| Boilerplate | ~30 righe di codice CVA | ~5 righe con model() |
// Futuro: nessun ControlValueAccessor, nessun provider
@Component({
selector: 'app-star-rating',
standalone: true,
template: `
<div class="star-rating">
@for (star of stars(); track star) {
<button
class="star"
[class.filled]="star <= value()"
(click)="value.set(star)">
★
</button>
}
</div>
`
})
export class StarRatingComponent {
// model() gestisce tutto: two-way binding, notifiche, stato
value = model(0);
maxStars = input(5);
stars = computed(() =>
Array.from({ length: this.maxStars() }, (_, i) => i + 1)
);
// Fine. Nessun CVA, nessun provider, nessuna callback.
}
// Nel parent:
// <app-star-rating [(value)]="rating.value" />
Migrazione Pianificata: Cosa Aspettarsi
I Signal Forms sono attualmente in fase di RFC e il team Angular ha comunicato che la loro introduzione sarà graduale e non breaking. I Reactive Forms continueranno a essere supportati per diversi major release, permettendo una migrazione progressiva.
Timeline Prevista della Migrazione
| Fase | Versione stimata | Cosa aspettarsi |
|---|---|---|
| RFC e feedback | Angular 19-20 | Raccolta feedback dalla community, iterazione sull'API |
| Developer Preview | Angular 20-21 | API disponibile ma soggetta a cambiamenti |
| Stabile | Angular 21-22 | API stabile, schematic di migrazione ufficiale |
| Raccomandato | Angular 22+ | Signal Forms come approccio predefinito nella documentazione |
| Deprecazione RF | Angular 24+ | Reactive Forms marcati come deprecated (con supporto esteso) |
Strategie di Preparazione alla Migrazione
- Adotta i Typed Forms: Se ancora usi FormControl senza tipizzazione, migra ai Typed Reactive Forms introdotti in Angular 14 come primo passo
- Usa
nonNullable: Configura i FormControl connonNullable: trueper allinearti al comportamento dei Signal Forms - Isola la logica form: Sposta la creazione e la gestione dei form in service dedicati, cosi la migrazione coinvolgera solo il service e non i componenti
- Riduci l'uso di
valueChanges: Dove possibile, usatoSignal(control.valueChanges)per iniziare a lavorare con Signal - Preferisci funzioni di validazione pure: Scrivi i validatori come funzioni pure che restituiscono errori, non come classi con dipendenze complesse
- Evita estensioni di AbstractControl: Non creare sottoclassi di FormControl o FormGroup, perchè non avranno equivalenti nei Signal Forms
- Documenta le regole di validazione: Mantieni una mappatura chiara tra campi e validatori per facilitare la conversione
// Service che isola la logica del form
// Oggi: usa Reactive Forms internamente, espone Signal
// Domani: sostituisci l'implementazione con Signal Forms
@Injectable({ providedIn: 'root' })
export class ProfileFormService {
private fb = inject(FormBuilder);
// Implementazione interna con Reactive Forms
private _form = this.fb.nonNullable.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
bio: ['', Validators.maxLength(500)]
});
// API pubblica basata su Signal
readonly name = toSignal(
this._form.controls.name.valueChanges,
{ initialValue: '' }
);
readonly email = toSignal(
this._form.controls.email.valueChanges,
{ initialValue: '' }
);
readonly isValid = toSignal(
this._form.statusChanges.pipe(map(s => s === 'VALID')),
{ initialValue: false }
);
readonly isDirty = toSignal(
this._form.valueChanges.pipe(map(() => this._form.dirty)),
{ initialValue: false }
);
// Metodi pubblici che restano invariati dopo la migrazione
setValues(data: Partial<ProfileData>) {
this._form.patchValue(data);
}
getValues(): ProfileData {
return this._form.getRawValue();
}
reset() {
this._form.reset();
}
}
Best Practices per i Form in Angular
Indipendentemente dal fatto che tu stia usando Template-driven Forms, Reactive Forms o ti stia preparando per i Signal Forms, alcune best practices sono universali e ti aiuteranno a scrivere form più robusti, manutenibili e pronti per il futuro.
Le 10 Best Practices per i Form Angular
- Scegli il tipo di form giusto: Template-driven per form semplici (login, contatti), Reactive/Signal per form complessi (multi-step, dinamici, con validazione avanzata)
- Isola la logica in service: Non creare FormGroup direttamente nel componente. Usa un service dedicato che espone Signal, facilitando test e riuso
- Validazione declarativa: Definisci i validatori come funzioni pure e riutilizzabili, non come logica inline nei componenti
-
Mostra errori solo dopo il touch: Non mostrare messaggi di errore fino a
quando l'utente non interagisce con il campo (
isTouched()oisDirty()) - Gestisci lo stato di loading: Disabilita il bottone di submit durante la validazione asincrona e mostra un indicatore di caricamento
-
Usa
nonNullable: Configura sempre i FormControl connonNullable: trueper evitare valorinullinaspettati dopo il reset -
Preferisci computed a effect: Deriva lo stato del form (validita, errori,
completamento) con
computed()piuttosto che coneffect()osubscribe() - Testa i validatori isolatamente: Scrivi unit test per ogni validatore personalizzato come funzione pura, indipendente dal componente
- Gestisci il submit con cura: Previeni doppi submit, gestisci errori HTTP, mostra feedback chiaro all'utente (successo/errore)
-
Prepara la migrazione: Se usi Reactive Forms, converti gradualmente
valueChangesin Signal contoSignal()per facilitare il passaggio futuro
Quando Usare Quale Tipo di Form
| Scenario | Template-Driven | Reactive Forms | Signal Forms (futuro) |
|---|---|---|---|
| Form login/contatto semplice | Consigliato | Accettabile | Consigliato |
| Form multi-step / wizard | Sconsigliato | Consigliato | Consigliato |
| Form dinamici (campi generati) | Sconsigliato | Consigliato | Consigliato |
| Validazione cross-field complessa | Difficile | Gestibile | Semplice |
| Form con stato derivato ricco | Limitato | Con RxJS | Nativo (computed) |
| Integrazione con Signals | Parziale | Con toSignal() | Nativa |
| Performance (change detection) | Zone.js dipendente | Zone.js dipendente | Fine-grained |
Anti-Pattern da Evitare nei Form
- Non usare
ngModelcon Reactive Forms: Mescolare template-driven e reactive nello stesso form causa comportamenti imprevedibili - Non dimenticare il cleanup: Con i Reactive Forms, ogni
subscribe()avalueChangesdeve essere gestito contakeUntilDestroyed()oDestroyRef - Non validare solo lato client: La validazione lato client migliora la UX, ma la validazione lato server e obbligatoria per la sicurezza
- Non ignorare l'accessibilità: Usa
aria-describedbyper collegare messaggi di errore ai campi, earia-invalidper segnalare campi non validi - Non usare
getRawValue()senza motivo: Se il form non ha campi disabled,.valuee sufficiente
Riepilogo e Prossimi Passi
I Signal Forms rappresentano il passo naturale nell'evoluzione di Angular verso un framework completamente signal-based. Eliminando la dipendenza da RxJS per lo stato dei form, riducendo il boilerplate e integrando nativamente la reattività fine-grained, i Signal Forms promettono di rendere la gestione dei form più semplice, più sicura e più performante.
Concetti Chiave di Questo Articolo
- Limiti Reactive Forms: Dipendenza da RxJS, boilerplate eccessivo, stato mutabile, gerarchia AbstractControl rigida, nessuna integrazione con Signals
- Signal Forms RFC: Nuove primitive (
formSignal,formRecord,formList) basate su Signals per sostituire FormControl, FormGroup, FormArray - Stato reattivo:
isValid(),isDirty(),isTouched(),errors()come computed signal sempre coerenti - Validazione semplificata: Validatori come funzioni pure, debounce integrato per async, cross-field reattivo
- Composizione con formRecord: Stato propagato automaticamente dai figli al padre, reset atomico, tipizzazione inferita
- model() + Signal Forms: Two-way binding senza ControlValueAccessor, componenti form riutilizzabili e minimali
- Approccio ibrido: Usare
toSignal()con Reactive Forms oggi per prepararsi alla migrazione - CVA semplificato: In futuro,
model()sostituira completamente l'interfaccia ControlValueAccessor - Migrazione graduale: Reactive Forms resteranno supportati a lungo, la migrazione sarà assistita da schematic ufficiali
Nel prossimo articolo della serie esploreremo SSR e Incremental Hydration, le tecniche di Server-Side Rendering che permettono ad Angular di raggiungere performance di caricamento eccellenti, con l'hydration incrementale che riduce il Time to Interactive trasferendo la logica dal server al client in modo progressivo.
Domande frequenti
I Reactive Forms di Angular sono ancora supportati dopo l'introduzione dei Signal Forms?
Sì, i Reactive Forms continueranno a essere supportati per diversi major release, permettendo una migrazione progressiva.
Come funziona la validazione con i Signal Forms rispetto ai Reactive Forms attuali?
I validatori diventano funzioni pure che restituiscono Signal di errori, con supporto nativo per validazione sincrona e asincrona senza bisogno di interfacce separate.
Come si possono utilizzare i Signals con i Reactive Forms attuali per prepararsi alla migrazione verso i Signal Forms?
È possibile iniziare a sfruttare i Signals con i Reactive Forms attuali, adottando un approccio ibrido che facilità la futura migrazione. Questo significa usare per convertire gli Observable dei form in Signals e costruire lo stato derivato con.
I Signal Forms sono compatibili con il two-way binding utilizzando la funzione model()?
Sì, i Signal Forms si integrano perfettamente con , creando un flusso di dati bidirezionale pulito tra il template e lo stato del form senza bisogno di direttive speciali come.

Commenti
Caricamento commenti...
Accedi per commentare