
GUIDE PER ASPIRANTI PROGRAMMATORI
Invio di form in Angular
Parlando di routing abbiamo detto che una web app classica funziona come un ping pong tra client e server, e abbiamo visto come il frontend routing consente di spostare le logiche di navigazione lato client in modo da evitare continui ricarichi della pagina. Ebbene la navigazione non è l’unica ragione per cui il browser…


Vuoi avviare una nuova carriera o fare un upgrade?
Trova il corso Digital & Tech più adatto a te nel nostro catalogo!
- Le direttive in Angular
- I componenti in Angular
- Il template in Angular
- Le direttive strutturali in Angular
- La content projection in Angular
- I servizi in Angular
- Le Pipes in Angular
- Routing in Angular
- Invio di form in Angular
- Built-in control flow in Angular
- Deferrable views in Angular
- Image optimization in Angular
Parlando di routing abbiamo detto che una web app classica funziona come un ping pong tra client e server, e abbiamo visto come il frontend routing consente di spostare le logiche di navigazione lato client in modo da evitare continui ricarichi della pagina.
Ebbene la navigazione non è l’unica ragione per cui il browser normalmente ricarica una pagina web: un’altra ragione può essere l’invio di un form (o modulo). Lavorando in linguaggio HTML puro, senza l’aiuto del linguaggio JavaScript, il tag form è sostanzialmente l’unico modo che abbiamo per interagire con l’applicazione, ma il submit di un form coincide con una richiesta POST al server e una conseguente redirezione alla pagina che segue l’invio del modulo.
Nelle moderne web app interattive, assumere il controllo sulle logiche di compilazione e invio di un form è importantissimo per garantire una user experience (UX) ottimale. Anche in questo ambito Angular non ci lascia soli e ci mette a disposizione due diversi ng module che rappresentano due diversi approcci nella gestione dei form: FormsModule e ReactiveFormsModule.
Come accade rispetto ad altre API del framework, anche in questo caso la differenza tra i due approcci è principalmente la seguente: in un caso il framework si fa carico di una parte del carico di lavoro e ci rende la vita più facile, al costo di un minore controllo sulle logiche di funzionamento del form.
In questo caso, l’API più di basso livello è ReactiveFormsModule, fortemente basata anch’essa su RxJS e sull’uso di Observable per esporre le modifiche che l’utente apporta al form, da cui il nome reactive forms; su questa API si basa FormsModule, che incapsula il modello reattivo in una serie di direttive da applicare nel template del form, da cui il nome template driven forms.
Prendiamo ad esempio un form di registrazione con i seguenti campi:
- username: obbligatorio, univoco;
- password: obbligatorio, minimo 6 caratteri, almeno una lettera, almeno un numero;
- verifyPassword: contenuto uguale a quello di password.
Nel caso in cui i campi falliscano la validazione, dovranno comparire dei messaggi sotto ai campi interessati.
Prima di partire, facciamo qualche osservazione:
- il campo username dovrà essere validato sia in modo sincrono, sia in modo asincrono; la validazione asincrona consiste in una chiamata HTTP a un endpoint che restituirà un booleano che ci dice se l’username inserito rispetta o no il vincolo di unicità, cioè in sostanza se il nome utente scelto è disponibile;
- il campo password dovrà attraversare una serie di passaggi di validazione tutti basati sul suo contenuto, mentre la validazione del campo verifyPassword implicherà un confronto tra due diversi campi del form.
Template driven forms in Angular.
Per prima cosa implementiamo il form in forma template driven.
In app.component.ts:
import { JsonPipe } from '@angular/common'; import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-root', standalone: true, imports: [FormsModule, JsonPipe], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent { model = { username: '', password: '', verifyPassword: '' } }
Qui abbiamo semplicemente inizializzato il model del nostro form, cioè un semplice oggetto che contiene i dati del form.
Abbiamo anche importato le direttive che useremo nel template in app.component.html:
<h2>Sign up!</h2> <form #signup="ngForm" appVerifyPassword> <div class="field"> <label for="username">Username</label> <input type="text" name="username" [(ngModel)]="model.username" #username="ngModel" required appUniqueUsername > @if (signup.submitted && username.hasError('required')) { <p class="error">This field is required.</p> } @if (signup.submitted && username.hasError('uniqueUsername')) { <p class="error">This username is not available.</p> } </div> <div class="field"> <label for="password">Password</label> <input type="text" name="password" [(ngModel)]="model.password" #password="ngModel" required minlength="6" appPasswordConstraints > @if (signup.submitted && password.hasError('required')) { <p class="error">This field is required.</p> } @if (signup.submitted && password.hasError('minlength')) { <p class="error">Password must be at least 6 characters long.</p> } @if (signup.submitted && password.hasError('letter')) { <p class="error">Password must contain at least one letter.</p> } @if (signup.submitted && password.hasError('digit')) { <p class="error">Password must contain at least one digit.</p> } </div> <div class="field"> <label for="verifyPassword">Verify password</label> <input type="text" name="verifyPassword" [(ngModel)]="model.verifyPassword" > @if (signup.submitted && signup.hasError('verifyPassword')) { <p class="error">Double check your password.</p> } </div> <button type="submit">Submit</button> </form> <pre>{{ model | json }}</pre>
Ora si che abbiamo un po’ di cose da spiegare!
Tanto per cominciare, abbiamo costruito un normalissimo form HTML standard, con campi input e le rispettive label.
Su questo abbiamo aggiunto:
- degli speciali binding a doppia via (in inglese: two-way data binding) con la direttiva attributo ngModel, usando una sintassi che combina data binding e event binding per fare in modo che la proprietà model del componente sia sempre sincronizzata con i valori del form;
- le template variable signup, username e password, necessarie per ispezionarne lo stato e mostrare i messaggi di errore, direttamente dal template; abbiamo usato la sintassi #variableName=”directiveSelector” per specificare il tipo di riferimento che vogliamo associare alla variabile, cioè ngModel per i campi singoli e ngForm per l’intero form;
- dei messaggi condizionati alla presenza di errori, mostrati solo dopo il primo tentativo di submit del form; per farlo abbiamo usato la sintassi di control-flow anziché le direttive strutturali, ma sarebbe stato equivalente;
- gli attributi di validazione HTML standard required e maxlength, che Angular intercetta con delle sue direttive in modo tale da integrarli nella validazione del form;
- gli attributi di validazione custom appVerifyPassword, appUniqueUsername e appPasswordConstraints, che dovranno essere associati a delle opportune direttive, che andremo presto a creare.
La sintassi del two-way binding è anche detta “banana in a box”, dalla forma delle parentesi [()]. Questa speciale sintassi è supportata da Angular a partire da una convenzione: per implementarla, un componente o una direttiva deve avere una input property con un certo nome, più una output property con lo stesso nome seguito dal suffisso Change.
Per esempio, la direttiva ngModel espone una proprietà @Input() ngModel e una proprietà @Output() ngModelChange. La sintassi [(ngModel)]=”model.username” è dunque semplicemente zucchero sintattico per [ngModel]=”model.username” (ngModelChange)=”model.username = $event”!
Prima di proseguire, osserviamo anche che:
- tutti gli input hanno l’attributo name; questo è necessario per consentire ad Angular di dare un nome ai campi e associarli correttamente al form;
- in coda al form abbiamo anche aggiunto un riferimento a model, rappresentato come oggetto JSON, per verificare che il two-way binding stia funzionando.
Definiamo anche un minimo di stili per rendere più funzionale la UI del nostro form;
in app.component.css:
:host { display: grid; justify-content: center; } h2 { text-align: center; } form { width: 400px; display: flex; flex-direction: column; gap: 20px; } p { margin: 0; } button { height: 40px; } .field { display: flex; flex-direction: column; gap: 5px; } .error { color: red; font-size: small; } /* highlight invalid fields after submit */ .ng-submitted > .field:has(.ng-invalid) { color: red; }
Qui l’unica cosa interessante da notare – ma non è cosa da poco – è che Angular mette a disposizione tutta una serie di classi che indicano lo stato del form; in questo modo possiamo stilizzarlo opportunamente in caso di errori.
Quello che abbiamo fatto è seguire con gli stili la stessa logica di condizionamento dei messaggi di errore: mentre l’utente compila il form non mostriamo errori, dopo il primo tentativo di submit li mostriamo e da quel momento li faremo sparire solo se i campi verranno opportunamente compilati.
Un’altra strategia potrebbe essere aspettare che l’utente modifichi un campo e, dunque, mostrare subito gli errori, prima del submit. Per farlo, possiamo usare le classi ng-dirty e ng-touched: la prima comparirà alla prima modifica del form, la seconda solo dopo aver tolto il focus al primo campo. Sulla template variable del form avremo le proprietà omologhe dirty e touched, che potremo usare al posto di submitted per condizionare i messaggi d’errore.
È giunto il momento di implementare le direttive di validazione. Per crearne una, dobbiamo sostanzialmente implementare l’interfaccia Validator e configurare un particolare tipo di provider, cioè una configurazione per consentire alla DI di associare l’istanza dell’interfaccia al form.
ng g d validation/password-constraints ng g d validation/verify-password ng g d validation/unique-username
Andiamo a implementarle.
Iniziamo con password-constraints.directive.ts:
import { Directive } from '@angular/core'; import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms'; @Directive({ selector: '[appPasswordConstraints]', standalone: true, providers: [{ // <- add provider provide: NG_VALIDATORS, useExisting: PasswordConstraintsDirective, multi: true }] }) export class PasswordConstraintsDirective implements Validator { validate(control: AbstractControl<any, any>): ValidationErrors | null { const hasLetter = /[a-zA-Z]/.test(control.value); const hasDigit = /[0-9]/.test(control.value); return (hasLetter && hasDigit) ? null : { letter: !hasLetter, digit: !hasDigit }; } }
Questa direttiva può restituire da nessuno a due errori. La presenza di errori viene marcata da proprietà con valore veritiero (truthy), in questo caso true; è anche possibile valorizzare la proprietà con un oggetto o una stringa, qualunque cosa sia d’aiuto per esempio per mostrare il messaggio di errore nel form.
Dunque, anche l’assenza di una proprietà sull’oggetto restituito implica l’assenza di un determinato errore.
Nel caso in cui non ci siano errori è importante restituire null, altrimenti il campo risulterà invalido anche se non conterrà alcuna flag.
Implementiamo ora verify-password.directive.ts:
import { Directive } from '@angular/core'; import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms'; @Directive({ selector: '[appVerifyPassword]', standalone: true, providers: [{ provide: NG_VALIDATORS, useExisting: VerifyPasswordDirective, multi: true }] }) export class VerifyPasswordDirective implements Validator { validate(control: AbstractControl<any, any>): ValidationErrors | null { const password = control.get('password'); const verifyPassword = control.get('verifyPassword'); return password?.value !== verifyPassword?.value ? { verifyPassword: true } : null; } }
In questo caso, è importante notare che la validazione non è stata applicata su un singolo campo, ma sull’intero form: i campi sono stati estratti tramite il metodo get, che consente di navigare tramite le stringhe utilizzate per popolare l’attributo name dei vari campi.
Il fatto che il modello del form sia stato costruito automaticamente a partire dal template HTML è il motivo essenziale per cui questo approccio è chiamato template driven.
Non ci resta che implementare unique-username.directive.ts:
import { Directive } from '@angular/core'; import { AbstractControl, AsyncValidator, NG_ASYNC_VALIDATORS, ValidationErrors } from '@angular/forms'; import { Observable, map, timer } from 'rxjs'; const mockUsernameDb = ['tim', 'john', 'anna']; @Directive({ selector: '[appUniqueUsername]', standalone: true, providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: UniqueUsernameDirective, multi: true }] }) export class UniqueUsernameDirective implements AsyncValidator { validate(control: AbstractControl<any, any>): Observable<ValidationErrors | null> { return timer(1000).pipe(map(() => mockUsernameDb.includes(control.value) ? { uniqueUsername: true } : null )); } }
Osserviamo subito che, in questo caso, l’interfaccia implementata è AsyncValidator anziché Validator, e come provider abbiamo usato il token NG_ASYNC_VALIDATORS anziché NG_VALIDATORS. Questo perché Angular deve sapere che dovrà aspettare prima di valutare il risultato della validazione.
Il metodo validate può ritornare un Observable o una Promise; in questo caso abbiamo ristretto il valore di ritorno a Observable.
Dato che non abbiamo un backend, lo mockiamo, cioè simuliamo il comportamento di una chiamata HTTP con un “database” fantoccio, rappresentato da un array contenente gli username già presi.
La flag d’errore si alza, dunque, dopo un secondo se il valore inserito è incluso nell’array.
Per simulare l’attesa di una chiamata HTTP abbiamo usato timer, una funzione che crea un Observable che emette dopo il tempo indicato, dopodiché abbiamo mappato l’evento con l’oggetto contenente la flag di errore. In alternativa, avremmo potuto creare una Promise e risolverla dopo un certo tempo usando setTimeout.
A questo punto le nostre direttive sono tutte completate; non ci resta che importarle in app.component.ts:
import { JsonPipe } from '@angular/common'; import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { PasswordConstraintsDirective } from './validation/password-constraints.directive'; import { VerifyPasswordDirective } from './validation/verify-password.directive'; import { UniqueUsernameDirective } from './validation/unique-username.directive'; @Component({ selector: 'app-root', standalone: true, imports: [ FormsModule, JsonPipe, PasswordConstraintsDirective, // <- VerifyPasswordDirective, // <- UniqueUsernameDirective // <- ], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent { model = { username: '', password: '', verifyPassword: '' } }
Ed ecco che, uniti tutti i tasselli, il nostro form soddisfa tutti i requisiti.
Abbiamo dunque visto le basi dei form con l’approccio template driven; non ci resta che implementare lo stesso esempio usando i reactive forms.
Reactive forms in Angular.
Iniziamo subito da app.component.ts:
import { JsonPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { validateVerifyPassword } from './validation/validate-verify-password'; import { validateUniqueUsername } from './validation/validate-unique-username'; import { validatePasswordConstraints } from './validation/validate-password-constraints'; @Component({ selector: 'app-root', standalone: true, imports: [ReactiveFormsModule, JsonPipe], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent { model = inject(FormBuilder).group({ username: ['', Validators.required, validateUniqueUsername], password: ['', [ Validators.required, Validators.minLength(6), validatePasswordConstraints] ], verifyPassword: [''] }, { validators: [validateVerifyPassword] }); }
Ci accorgiamo immediatamente che model non è più un semplice oggetto, ma un’istanza della classe FormGroup, che fornisce tutta una serie di proprietà e metodi per interagire direttamente con le meccaniche interne del form.
Il FormGroup è stato costruito attraverso il servizio FormBuilder, che funziona così: per ogni campo richiede una tripla, cioè un array con 3 posizioni:
- alla prima vuole il valore iniziale del form, in questo caso una stringa vuota per tutti i campi;
- alla seconda, un validatore sincrono oppure un array di validatori sincroni;
- alla terza, un validatore asincrono oppure un array di validatori asincroni.
Se non abbiamo bisogno di validatori possiamo semplicemente accorciare la tripla omettendo le posizioni vuote, oppure possiamo riempirle con un array vuoto o con il valore null. Il funzionamento dei metodi di FormBuilder serve esclusivamente a omettere la ripetizione del costruttore FormGroup, la cui firma funziona esattamente come appena spiegato, eccetto per il fatto che i posti della tripla diventano i 3 argomenti da passare al costruttore.
Quanto ai validatori, sia built-in sia custom, vediamo che sono semplici funzioni passate nella configurazione del form.
Prima di vedere come cambia il template, andiamo subito a implementarli.
Partiamo da validate-unique-username.ts, che possiamo creare manualmente:
import { AsyncValidatorFn } from '@angular/forms'; import { timer, map } from 'rxjs'; const mockUsernameDb = ['tim', 'john', 'anna']; export const validateUniqueUsername: AsyncValidatorFn = control => timer(1000).pipe(map(() => mockUsernameDb.includes(control.value) ? { uniqueUsername: true } : null ));
Vediamo che qui non stiamo dichiarando una direttiva che implementa l’interfaccia AsyncValidator, ma più semplicemente stiamo esportando una funzione di tipo AsyncValidatorFn, che non è altro che un tipo che rappresenta la firma del metodo AsyncValidator.validate.
Vediamo ora validate-password-constraints.ts:
import { ValidatorFn } from '@angular/forms'; export const validatePasswordConstraints: ValidatorFn = control => { const hasLetter = /[a-zA-Z]/.test(control.value); const hasDigit = /[0-9]/.test(control.value); return (hasLetter && hasDigit) ? null : { letter: !hasLetter, digit: !hasDigit }; }
Qui abbiamo usato ValidatorFn al posto di AsyncValidatorFn, ma il parallelo con l’interfaccia Validator resta identico.
Chiudiamo con validate-verify-password.ts:
import { ValidatorFn } from '@angular/forms'; export const validateVerifyPassword: ValidatorFn = control => { const password = control.get('password'); const verifyPassword = control.get('verifyPassword'); return password?.value !== verifyPassword?.value ? { verifyPassword: true } : null; }
Anche qui, nessuna sorpresa.
Vediamo infine come è cambiato app.component.html:
<h2>Sign up!</h2> <form [formGroup]="model" #form="ngForm"> <div class="field"> <label for="username">Username</label> <input type="text" formControlName="username"> @if (form.submitted && model.controls.username.hasError('required')) { <p class="error">This field is required.</p> } @if (form.submitted && model.controls.username.hasError('uniqueUsername')) { <p class="error">This username is not available.</p> } </div> <div class="field"> <label for="password">Password</label> <input type="text" formControlName="password"> @if (form.submitted && model.controls.password.hasError('required')) { <p class="error">This field is required.</p> } @if (form.submitted && model.controls.password.hasError('minlength')) { <p class="error">Password must be at least 6 characters long.</p> } @if (form.submitted && model.controls.password.hasError('letter')) { <p class="error">Password must contain at least one letter.</p> } @if (form.submitted && model.controls.password.hasError('digit')) { <p class="error">Password must contain at least one digit.</p> } </div> <div class="field"> <label for="verifyPassword">Verify password</label> <input type="text" formControlName="verifyPassword"> @if (form.submitted && model.hasError('verifyPassword')) { <p class="error">Double check your password.</p> } </div> <button type="submit">Submit</button> </form> <pre>{{ model.value | json }}</pre>
Notiamo subito che il template si è snellito di parecchio, dato che la validazione non passa più attraverso una serie di attributi, ma è interamente regolata lato codice.
L’unico binding presente è quello su [formGroup]=”model”, dove abbiamo associato l’intero form alla proprietà model di tipo FormGroup.
Dopodiché, al posto di name usiamo l’attributo formControlName, e il gioco è fatto.
Per vedere il valore del form non usiamo più direttamente model, ma la proprietà value esposta da FormGroup.
Per concludere, osserviamo che per valutare lo stato submitted del form abbiamo usato la template variable form che espone tale proprietà, perché per qualche motivo inspiegabile FormGroup non lo fa.
La verità è che una spiegazione per l’assenza di questa proprietà su FormGroup ci sarebbe: poiché i form group sono annidabili uno dentro l’altro, e form group non è l’unico modo di aggregare diversi campi, non è esattamente sensato che lo stato di sottoscrizione di un form sia consultabile a partire da questa classe; perciò dobbiamo ripiegare sulla direttiva ngForm, la stessa che useremmo nei template driven forms e che è necessariamente associata a un tag form HTML.
Prima di proseguire, ci resta una domanda a cui rispondere: perché si chiamano reactive forms? La risposta è molto semplice: perché, similarmente a quanto visto per ActivatedRoute, FormGroup espone una serie di eventi attraverso le proprietà di tipo Observable valueChanges e statusChanges.
Vediamolo rapidamente in app.component.ts:
import { JsonPipe } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { validateVerifyPassword } from './validation/validate-verify-password'; import { validateUniqueUsername } from './validation/validate-unique-username'; import { validatePasswordConstraints } from './validation/validate-password-constraints'; @Component({ selector: 'app-root', standalone: true, imports: [ReactiveFormsModule, JsonPipe], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent implements OnInit { model = inject(FormBuilder).group({ username: ['', Validators.required, validateUniqueUsername], password: ['', [ Validators.required, Validators.minLength(6), validatePasswordConstraints ]], verifyPassword: [''] }, { validators: [validateVerifyPassword] }); ngOnInit(): void { // <- this.model.valueChanges.subscribe(value => console.log({ value })); this.model.statusChanges.subscribe(status => console.log({ status })) } }
Se ora apriamo la console del browser e guardiamo cosa succede mentre compiliamo il form, vedremo che ad ogni nostra azione riceveremo aggiornamenti su valore e stato del form.
Abbiamo visto come realizzare un form con validazione sincrona e asincrona secondo i due approcci messi a disposizione da Angular, ma abbiamo – fin qui – omesso la cosa più semplice, ma più importante: la gestione dell’evento submit: in questo caso, sia che usiamo FormsModule o ReactiveFormsModule, sarà sufficiente agganciarsi all’evento ngSubmit sul tag form.
Questo evento verrà scatenato ogni volta che un button type=”submit” verrà cliccato, oppure se viene premuto invio. L’evento ngSubmit va usato al posto del semplice submit, anch’esso disponibile, perché tiene conto di tutta la gestione del form model.
Se, invece, volessimo semplicemente utilizzare il tag form senza usare né template driven forms né reactive forms, potremmo utilizzare direttamente submit.
CONTENUTI GRATUITI IN EVIDENZA
Guide per aspiranti programmatori 👨🏻🚀
Vuoi muovere i primi passi nel Digital e Tech? Abbiamo preparato alcune guide per aiutarti a orientarti negli ambiti più richiesti oggi.