BACK TO THE SERIESGuida galattica ad Angular · 銀河のガイド

Angular in 10 steps - 6 of 10

angular in 10 steps 6 of 10

06 - Forms: raccogliere dati senza impazzire


Quasi ogni applicazione, prima o poi, deve chiedere qualcosa all'utente: un login, una ricerca, un checkout. I form sono il punto dove i dati entrano nell'app, e infatti sono anche il punto dove si annidano più bug: input non validati, errori mostrati al momento sbagliato, submit che partono a vuoto.


Angular offre due approcci per gestirli:


  • Template-driven forms: si scrive quasi tutto nell'HTML, ideali per form semplici
  • Reactive forms: il modello del form vive nel TypeScript, pensati per form complessi

In questo capitolo li vediamo entrambi, con le validazioni e qualche consiglio su quando scegliere l'uno o l'altro.


🧩 Template-driven: il form nell'HTML


L'approccio template-driven si appoggia alla direttiva ngModel, che conosci già dal capitolo sul data binding: è il two-way binding applicato ai campi di un form. Il grosso del lavoro sta nel template, il componente resta leggero.

TYPESCRIPT · コード
1import { Component, signal } from '@angular/core';
2import { FormsModule } from '@angular/forms';
3
4@Component({
5  selector: 'app-iscrizione',
6  imports: [FormsModule],
7  templateUrl: './iscrizione.component.html'
8})
9export class IscrizioneComponent {
10  nome = signal('');
11
12  submit() {
13    console.log('inviato:', this.nome());
14  }
15}
HTML · コード
1<form (ngSubmit)="submit()">
2  <label>
3    Nome:
4    <input name="nome" [(ngModel)]="nome" />
5  </label>
6  <p>Ciao, {{ nome() }}!</p>
7
8  <button type="submit">Invia</button>
9</form>

Due dettagli che fanno inciampare tutti la prima volta:

Al two-way binding si passa il signal, non la sua chiamata: si scrive [(ngModel)]="nome", mentre {{ nome() }} resta una lettura e vuole le parentesi. E dentro un <form>, ogni campo con ngModel deve avere l'attributo name, altrimenti Angular protesta.

Validare un form template-driven


La tentazione, dopo il capitolo sui signals, sarebbe costruirsi la validazione a mano con un paio di computed. Si può fare, ma non serve: Angular tiene già la contabilità di ogni campo, se è stato toccato (touched), se è cambiato (dirty), se è valido, e la espone attraverso lo stesso ngModel. Basta chiederla con una template reference variable:

HTML · コード
1<form (ngSubmit)="submit()">
2  <label>
3    Nome:
4    <input
5      name="nome"
6      [(ngModel)]="nome"
7      required
8      minlength="3"
9      #nomeCtrl="ngModel"
10    />
11  </label>
12
13  @if (nomeCtrl.touched && nomeCtrl.invalid) {
14    <div class="errore">
15      @if (nomeCtrl.hasError('required')) {
16        <p>Il campo è obbligatorio</p>
17      }
18      @if (nomeCtrl.hasError('minlength')) {
19        <p>Servono almeno 3 caratteri</p>
20      }
21    </div>
22  }
23
24  <button type="submit">Invia</button>
25</form>

Cosa sta succedendo:


  • required e minlength sono normali attributi HTML che Angular trasforma in validatori
  • #nomeCtrl="ngModel" ci dà accesso allo stato del campo direttamente nel template
  • gli errori compaiono solo dopo che il campo è stato toccato: niente form rosso al primo caricamento

Per form di due o tre campi, questo approccio è onesto e veloce. Quando i campi diventano dieci, o si aggiungono e tolgono dinamicamente, il template si trasforma in un groviglio. Ed è lì che entrano in gioco i reactive forms.


🔧 Reactive forms: il form nel TypeScript


Nei reactive forms il modello del form è un oggetto che costruisci nel componente, con tre mattoni:


  • FormControl: un singolo campo
  • FormGroup: un gruppo di controlli, tipicamente l'intero form
  • FormArray: una lista dinamica di controlli, per i campi che si aggiungono a runtime

Il template si limita a collegarsi al modello. Ecco un form di profilo con validazioni e una lista dinamica di contatti:

TYPESCRIPT · コード
1import { Component } from '@angular/core';
2import {
3  FormArray,
4  FormControl,
5  FormGroup,
6  ReactiveFormsModule,
7  Validators,
8} from '@angular/forms';
9
10@Component({
11  selector: 'app-profilo',
12  imports: [ReactiveFormsModule],
13  templateUrl: './profilo.component.html'
14})
15export class ProfiloComponent {
16  profiloForm = new FormGroup({
17    nome: new FormControl('', [Validators.required, Validators.minLength(3)]),
18    email: new FormControl('', [Validators.required, Validators.email]),
19    contatti: new FormArray([new FormControl('', Validators.email)]),
20  });
21
22  get nome() {
23    return this.profiloForm.get('nome');
24  }
25
26  get email() {
27    return this.profiloForm.get('email');
28  }
29
30  get contatti() {
31    return this.profiloForm.get('contatti') as FormArray;
32  }
33
34  aggiungiContatto() {
35    this.contatti.push(new FormControl('', Validators.email));
36  }
37
38  onSubmit() {
39    if (this.profiloForm.valid) {
40      console.log(this.profiloForm.value);
41    } else {
42      // rende visibili gli errori di tutti i campi
43      this.profiloForm.markAllAsTouched();
44    }
45  }
46}

E il template:

HTML · コード
1<form [formGroup]="profiloForm" (ngSubmit)="onSubmit()">
2  <label>
3    Nome:
4    <input type="text" formControlName="nome" />
5  </label>
6  @if (nome?.touched && nome?.invalid) {
7    <div class="errore">
8      @if (nome?.hasError('required')) {
9        <p>Il nome è obbligatorio</p>
10      }
11      @if (nome?.hasError('minlength')) {
12        <p>Servono almeno 3 caratteri</p>
13      }
14    </div>
15  }
16
17  <label>
18    Email:
19    <input type="email" formControlName="email" />
20  </label>
21  @if (email?.touched && email?.invalid) {
22    <div class="errore">
23      @if (email?.hasError('required')) {
24        <p>L'email è obbligatoria</p>
25      }
26      @if (email?.hasError('email')) {
27        <p>Il formato non è valido</p>
28      }
29    </div>
30  }
31
32  <div formArrayName="contatti">
33    @for (contatto of contatti.controls; track $index) {
34      <input [formControlName]="$index" placeholder="Contatto {{ $index + 1 }}" />
35    }
36  </div>
37  <button type="button" (click)="aggiungiContatto()">Aggiungi contatto</button>
38
39  <button type="submit">Invia</button>
40</form>

Nota il flusso del submit: il bottone è sempre cliccabile e, se il form non è valido, markAllAsTouched() fa comparire tutti gli errori. È una scelta deliberata: disabilitare il bottone con [disabled]="profiloForm.invalid" è comodo, ma lascia l'utente davanti a un bottone spento senza spiegargli il perché. Un click che rivela gli errori è un'esperienza più chiara.

Dai tempi di Angular 14 i reactive forms sono tipizzati: il compilatore sa che profiloForm.value.nome è una stringa e ti segnala l'errore se provi a leggere un campo che non esiste. Se il new FormGroup(...) a mano ti sembra verboso, guarda anche FormBuilder: iniettato con inject(FormBuilder), riduce la cerimonia con la sintassi fb.group({...}).

Quale approccio scegliere


La mia bussola, dopo anni di form:


  • Template-driven quando il form è piccolo, statico e le validazioni sono quelle standard HTML: login, ricerca, newsletter
  • Reactive quando ci sono campi dinamici, validazioni incrociate, logica condizionale o quando il form va testato per bene: il modello in TypeScript si testa senza toccare il DOM

Nel dubbio, reactive: un template-driven che cresce si riscrive, un reactive sovradimensionato al massimo ha qualche riga in più.


I validator pronti all'uso


Angular fornisce una serie di validatori già fatti, utilizzabili sia come attributi (template-driven) sia da Validators (reactive):


  • Validators.required: il campo non può essere vuoto
  • Validators.email: formato email
  • Validators.minLength(n) / Validators.maxLength(n): lunghezza minima e massima
  • Validators.min(n) / Validators.max(n): valori numerici
  • Validators.pattern(regex): qualsiasi cosa esprimibile con una regex

Quando non bastano, puoi scrivere validatori custom: sono semplici funzioni che ricevono il controllo e restituiscono un oggetto errori o null. Ci torneremo quando serviranno.


Dove siamo arrivati


Ora sai raccogliere dati in entrambi i modi che Angular mette a disposizione: template-driven con ngModel e lo stato esposto nel template, reactive con il modello in TypeScript, le validazioni programmatiche e i campi dinamici via FormArray. E soprattutto sai quando usare l'uno e quando l'altro.


Nel prossimo capitolo parliamo di servizi e dependency injection: dove mettere la logica e i dati che i componenti devono condividere, a partire da quel inject() che continua a spuntare negli esempi.