Il rilascio di Angular v18 ha portato una serie di nuove funzionalità e miglioramenti al framework.
Una di queste funzionalità è particolarmente promettente, in quanto introduce una nuova funzionalità all’interno della libreria Angular Forms, aggiungendo nella classe AbstractControl un evento unico per i cambiamenti di stato.
Come di consueto nei miei articoli, prima di approfondire l’argomento principale, ripassiamo un pò le basi. Questo ci aiuterà a comprendere meglio i contenuti dell’articolo.
Angular Reactive Forms: le basi
I Reactive Forms di Angular offrono un approccio basato su classi per gestire gli input dei form, fornendo accesso sincrono al modello di dati, potenti strumenti per la validazione degli input e il tracciamento delle modifiche tramite Observables.
Il modello di dati dei Reactive Forms è composto dalle seguenti classi:
FormControl
: rappresenta un singolo input del form, il suo valore è una primitiva;FormGroup
: rappresenta un gruppo di FormControl, il suo valore è un oggetto;FormArray
: rappresenta un elenco di FormControl, il suo valore è un array.
Un esempio comune di un form può essere rappresentato da un FormGroup come questo:
import { FormGroup, FormControl, FormArray } from '@angular/forms';
const articleForm = new FormGroup({
title: new FormControl(''),
content: new FormControl(''),
tags: new FormArray([])
});
Code language: TypeScript (typescript)
Nota: esiste anche la classe FormRecord, un’estensione della classe FormGroup, che permette di creare dinamicamente un gruppo di istanze di FormControl.
Tutte queste classi, d’ora in poi denominate semplicemente controlli, derivano dalla classe AbstractControl
e quindi condividono proprietà e metodi comuni.
Template binding
L’approccio basato su classi dei Reactive Forms è supportato da varie direttive fornite dalla stessa libreria, che facilitano l’integrazione dei controlli con gli elementi HTML.
Prendiamo il seguente FormGroup
come esempio:
this.articleForm = new FormGroup({
author: new FormGroup({
name: new FormControl(''),
}),
tags: new FormArray([ new FormControl('Angular') ]),
});
Code language: TypeScript (typescript)
Possiamo facilmente collegarlo al template utilizzando le apposite direttive:
<form [formGroup]="articleForm">
<div formGroupName="author">
<input formControlName="name" />
</div>
<div formArrayName="tags">
<div *ngFor="let tag of tags.controls; index as i">
<input [formControlName]="i" />
</div>
</div>
</form>
Code language: HTML, XML (xml)
È importante notare, senza perderci in una spiegazione esaustiva ma fuori dall’ambito di questo articolo, che la FormGroupDirective ci consente di creare facilmente un pulsante per ripristinare il form (reset) e un pulsante per inviare il suo valore (submit):
<form [formGroup]="articleForm">
<!-- form template -->
<button type="reset">Clear</button>
<button type="submit">Save</button>
</form>
Code language: HTML, XML (xml)
La FormGroupDirective intercetta gli eventi di click emessi da questi pulsanti per attivare la funzione reset()
del controllo, che riporta il controllo al suo valore iniziale, e l’evento di output ngSubmit
della direttiva.
Come restare in ascolto dei cambiamenti
Per restare in ascolto dei cambiamenti di valore ed eseguire logica di business, possiamo sottoscriverci all’observable valueChanges
del controllo che desideriamo monitorare:
myControl.valueChanges.subscribe(value => {
console.log('New value:', value)
});
Code language: TypeScript (typescript)
Disabilitare un controllo
Ogni controllo può essere disabilitato, impedendo agli utenti di modificarne il valore. Questo stato replica il comportamento dell’attributo HTML disabled
.
Per realizzare ciò, possiamo creare un controllo già disabilitato oppure utilizzare le funzioni disable()
e enable()
per attivare e disattivare questo stato:
import { FormControl } from '@angular/forms';
const myControl = new FormControl({ value: '', disabled: true });
console.log(myControl.disabled, myControl.enabled) // true, false
myControl.enable();
console.log(myControl.disabled, myControl.enabled) // false, true
myControl.disable();
console.log(myControl.disabled, myControl.enabled) // true, false
Code language: TypeScript (typescript)
Come possiamo notare nell’esempio in alto, la classe AbstractControl
fornisce due proprietà dedicate per descrivere questo stato: disabled
e enabled
.
Validatori
Per imporre regole specifiche ed assicurarci che i tuoi controlli rispettino determinati criteri, possiamo specificare delle regole di validazione, chiamate anche validatori.
I validatori possono essere sincroni, come required
o minLength
, oppure asincroni per gestire la validazione che dipendono da risorse esterne:
import { FormControl, Validators } from '@angular/forms';
import { MyCustomAsyncValidators } from './my-custom-async-validators.ts';
const myFormControl = new FormControl('', {
validators: [ Validators.required, Validators.minLength(3) ],
asyncValidators: [ MyCustomAsyncValidators.validate ]
});
Code language: TypeScript (typescript)
In base a queste regole, la classe AbstractControl
fornisce anche alcune proprietà che descrivono lo stato di validità:
valid
: un booleano che indica se il valore del controllo ha superato tutti i test delle regole di validazione;invalid
: un booleano che indica se il valore del controllo non ha superato tutti i test delle regole di validazione. È l’opposto della proprietàvalid
;pending
: un booleano che indica se il valore del controllo è in fase di verifica di validazione.
FormControlStatus
Sia lo stato disabled
che gli stati delle validazioni sono interconnessi.
Infatti, essi derivano dalla proprietà status
, la quale è di tipo:
type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
Code language: TypeScript (typescript)
Nota: le proprietà
valid
,invalid
,pending
,enabled
edisabled
sono in realtà solo getters derivati dalla proprietàstatus
🤓
Pristine e Touched
La classe AbstractControl
fornisce anche diverse proprietà che descrivono come l’utente ha interagito con il form:
pristine
: un booleano che indica se il controllo è intatto, il che significa che non è ancora stato modificato;dirty
: un booleano che indica se il controllo è stato modificato;untouched
: un booleano che indica se il controllo non è ancora stato toccato, il che significa che non è stato ancora interagito con esso;touched
: un booleano che indica se il controllo è stato toccato.
Ora che abbiamo ripassato alcuni dei fondamenti dei Reactive Forms, è finalmente giunto il momento di introdurre l’argomento principale di questo articolo.
Il nuovo evento unico per i cambiamenti di stato
A partire da Angular v18, la classe AbstractControl
espone un nuovo observable events
per tracciare tutti gli eventi di cambiamento dello stato del form.
Grazie a questo, è possibile monitorare le classi FormControl
, FormGroup
e FormArray
attraverso i seguenti eventi: PristineEvent
, ValueChangeEvent
, StatusEvent
e TouchedEvent
.
myControl.events
.pipe(filter((event) => event instanceof PristineChangeEvent))
.subscribe((event) => console.log('Pristine:', event.pristine));
myControl.events
.pipe(filter((event) => event instanceof ValueChangeEvent))
.subscribe((event) => console.log('Value:', event.value));
myControl.events
.pipe(filter((event) => event instanceof StatusChangeEvent))
.subscribe((event) => console.log('Status:', event.status));
myControl.events
.pipe(filter((event) => event instanceof TouchedChangeEvent))
.subscribe((event) => console.log('Touched:', event.touched));
Code language: TypeScript (typescript)
Queste capacità sono molto potenti, specialmente perché, a parte il valueChange
, in passato non era cosi facile ed immediato tracciare i cambiamenti di stato.
In aggiunta a questo, la classe FormGroup
può anche emettere altri due eventi attraverso l’observable events
: FormSubmittedEvent
e FormResetEvent
.
myControl.events
.pipe(filter((event) => event instanceof FormSubmittedEvent))
.subscribe((event) => console.log('Submit:', event));
myControl.events
.pipe(filter((event) => event instanceof FormResetEvent))
.subscribe((event) => console.log('Reset:', event));
Code language: TypeScript (typescript)
Entrambi gli eventi FormSubmittedEvent
e FormResetEvent
sono ereditati dalla FormGroupDirective e sono infatti emessi dalla direttiva stessa.
Approfondimenti aggiuntivi
Grazie a questa nuova aggiunta, i seguenti metodi della classe AbstractControl
sono stati aggiornati per supportare il parametro emitEvent
:
markAsPristine()
: imposta il controllo comepristine
;markAsDirty()
: imposta il controllo comedirty
;markAsTouched()
: imposta il controllo cometouched
;markAsUntouched()
: imposta il controllo comeuntouched
;markAllAsTouched()
: imposta il controllo e tutti i suoi discendenti cometouched
.
Grazie per aver letto questo articolo 🙏
Mi piacerebbe avere qualche feedback quindi grazie in anticipo per qualsiasi commento. 👏
Infine, se ti è piaciuto davvero tanto, condividilo con la tua community. 👋😁