La gestione di form è uno degli aspetti più comuni in una applicazione Web. Dall'autenticazione dell'utente alla ricerca di informazioni, una qualsiasi raccolta di dati da elaborare richiede la progettazione di form HTML.
Angular ha da sempre avuto un occhio di riguardo per la gestione delle form, fornendo un supporto che cerca di semplificare l'acquisizione dei dati e la loro validazione. Questa tendenza si conferma anche con la versione 2 del framework che rivede un po' l'architettura interna mantenendo le funzionalità principali.
In particolare, Angular 2 propone due approcci alla costruzione e gestione di form:
Campo | Descrizione |
---|---|
Template Driven Form | Si basa prevalentemente sulla definizione di una form tramite markup, inclusi i criteri di validazione, in modo simile all'approccio di Angular 1.x |
Reactive Form | Detto anche Model Driven Form, questo approccio prevede una definizione minimale del template tramite markup e sposta la logica di validazione all'interno della definizione del componente. Inoltre prevede una modalità alternativa di costruzione della form stessa. |
Template Driven Form
A differenza di quanto accade in Angular 1.x, il supporto per la gestione di form non è disponibile di default. Indipendentemente dall'approccio che si intende utilizzare, è necessario abilitare componenti e direttive importandoli dai relativi moduli.
In particolare, per l'abilitazione delle Template Driven Form occorre importare FormsModule dal modulo @angular/forms all'interno del modulo della nostra applicazione, come mostrato dal seguente codice:
import { Component} from '@angular/core';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { ArticoloComponent } from './articolo';
import { ArticoloFormComponent } from './articolo-form';
@Component({...})
export class AppComponent {
...
}
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, ArticoloComponent, ArticoloFormComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Questa attività preliminare ci consente di avere a disposizione all'interno della nostra applicazione il supporto per la gestione di form secondo l'approccio Template Driven.
Vogliamo integrare la nostra applicazione per la consultazione di articoli con la possibilità di aggiungere un nuovo articolo. Creiamo quindi un nuovo componente tramite il comando ng generate
di Angular CLI:
ng generate component articolo-form
e adattiamo il codice generato alle nostre esigenze:
import { Component } from '@angular/core';
import { Articolo} from '../articolo';
@Component({
selector: 'articolo-form',
templateUrl: 'articolo-form.component.html',
styleUrls: ['articolo-form.component.css']
})
export class ArticoloFormComponent {
model = new Articolo();
constructor() {}
}
Abbiamo importato nel modulo la classe Articolo
ed abbiamo definito la proprietà model
per il componente ArticoloFormComponent
, assegnandole un'istanza di Articolo
.
Definiamo ora il template del nostro componente inserendo nel file articolo-form.component.html
il seguente markup:
<form>
<div>
<label for="txtTitolo">Titolo</label>
<input type="text" id="txtTitolo" required>
</div>
<div>
<label for="txtAutore">Autore</label>
<input type="text" id="txtAutore" required>
</div>
<div>
<label for="txtTesto">Testo</label>
<textarea id="txtTesto" required></textarea>
</div>
<button type="button">Invia</button>
</form>
Il risultato della definizione del componente ArticoloFormComponent
allo stato attuale si limita ad associare al markup <articolo-form></articolo-form>
la form per l'inserimento di un nuovo articolo. Il risultato visuale che otteniamo è analogo a questo:
Data binding
Questa definizione, però, non fa nulla di speciale se non definire la struttura della nostra form. Ciò che vorremmo ottenere sarebbe la possibilità di mappare il modello dei dati del componente, cioè la proprietà model
, sulla form e viceversa, cioè mappare il contenuto della form sul modello dei dati. Per ottenere questo possiamo ricorrere al two-way binding chiamando in causa la direttiva ngModel
. Vediamo come cambia il nostro markup:
<form>
<div>
<label for="txtTitolo">Titolo</label>
<input type="text" id="txtTitolo"
required [(ngModel)]="model.titolo" name="titolo">
</div>
<div>
<label for="txtAutore">Autore</label>
<input type="text" id="txtAutore"
required [(ngModel)]="model.autore" name="autore">
</div>
<div>
<label for="txtTesto">Testo</label>
<textarea id="txtTesto"
required [(ngModel)]="model.testo" name="testo"></textarea>
</div>
<button type="button">Invia</button>
</form>
Possiamo vedere come per ciascun elemento di input della form abbiamo aggiunto il riferimento alla direttiva ngModel
mappato sulla proprietà del modello che intendiamo associare. Abbiamo inoltre specificato l'attributo name
, fondamentale per la registrazione dell'elemento come componente della form da parte di Angular.
Non bisogna confondere l'attributo id
con l'attributo name
. Nel nostro esempio gli elementi della form hanno entrambi gli attributi, ma hanno ruoli diversi. L'attributo id
serve per l'associazione con la rispettiva label
mentre name
è richiesto per la gestione delle form e non è necessario che entrambi gli attributi abbiano lo stesso valore, come mostrato dal codice d'esempio.
Questa semplice modifica fa in modo che quello che viene inserito nella form venga automaticamente assegnato alla corrispondente proprietà del modello dei dati. Possiamo verificarlo aggiungendo in fondo al markup della form il seguente codice:
titolo: {{model.titolo}}<br/>
autore: {{model.autore}}<br/>
testo: {{model.testo}}<br/>
Vedremo che man mano che inseriamo dei valori nei campi della form, tali valori vengono mostrati in corrispondenza della proprietà associata.
Tracciamento dello stato e validazione
Naturalmente possiamo andare oltre il data binding e gestire lo stato della form e la validazione.
Per quanto riguarda il tracciamento dello stato della form, ngModel
aggiunge alcune interessanti proprietà a ciascun elemento di input ed alla form stessa che ci consentono di stabilire il suo stato di editing. Nello specifico, sono previste le seguenti coppie di proprietà booleane:
Proprietà | Descrizione |
---|---|
touched untouched |
Indicano se un elemento di input è stato visitato dall'utente, cioè se almeno una volta il campo ha perso il focus. Se l'elemento non ha mai perso il focus la sua proprietà touched ha valore false , mentre untouched è true ; in caso contrario i valori si invertono. |
dirty pristine |
Indicano se il valore di un campo è stato modificato rispetto al valore originario. Se non è stato modificato, il valore della proprietà dirty è false mentre quello della proprietà pristine è true ; i valori si invertono nella situazione opposta. |
valid invalid |
Queste proprietà indicano se i valori presenti nella form sono validi secondo i criteri di validazione impostati. Come è naturale, valid e invalid prendono il valore booleano corrispondente nel caso in cui un campo sia valido o meno. |
Possiamo sfruttare queste proprietà direttamente nel markup per gestire opportunamente l'attivazione o disattivazione di elementi grafici o per visualizzare messaggi informativi.
Ad esempio, dal momento che tutti gli elementi della nostra form sono obbligatori avendo l'attributo required
, possiamo mostrare abilitare il pulsante di invio soltanto quando l'intera form risulta valida. Per ottenere questo dobbiamo per prima cosa associare una reference
alla form associandola alla direttiva ngForm
ed utilizzare questa reference
per gestire lo stato di abilitazione del pulsante di invio, come mostrato di seguito:
<form #myForm="ngForm">
...
<button type="submit" [disabled]="!myForm.valid">Invia</button>
</form>
La direttiva ngForm
viene automaticamente associata ad una form, quindi normalmente non abbiamo bisogno di fare riferimento ad essa, ma per accedere alle proprietà speciali aggiunte da Angular dobbiamo necessariamente esplicitarla associandola ad una reference
come abbiamo appena visto.
Analogamente, per gestire le proprietà di tracciamento dello stato di un campo occorre definire una reference
ed assegnarla alla direttiva ngModel
. Il seguente esempio mostra come visualizzare un messaggio che notifica l'obbligo di inserimento di un titolo per l'articolo:
<form #myForm="ngForm">
<div>
<label for="txtTitolo">Titolo</label>
<input type="text" id="txtTitolo" required [(ngModel)]="model.titolo" name="titolo" #myTitolo="ngModel">
<span [hidden]="myTitolo.valid || myTitolo.pristine">Il titolo é obbligatorio!</span>
</div>
...
<button type="button" [disabled]="!myForm.valid">Invia</button>
</form>
Abbiamo definito la reference myTitolo
che ci consente di stabilire se visualizzare o meno il messaggio in base al valore delle proprietà valid
e pristine
.
Oltre ad impostare i valori delle sei proprietà di tracciamento dello stato di editing, la direttiva ngModel
associa alcune classi CSS predefinite agli elementi gestiti. Le classi prendono lo stesso nome della proprietà il cui valore corrente è true
preceduto dal prefisso ng-
. Ad esempio, se il valore corrente della proprietà valid
è true
, e di conseguenza invalid
è false
, verrà assegnato al campo la classe CSS ng-valid
.
Angular non definisce nessuna regola CSS che modifica l'aspetto grafico dell'elemento, si limita soltanto ad assegnare la classe. Noi possiamo definire le regole CSS per le classi di Angular in modo da evidenziare graficamente lo stato di editing dei vari elementi della form.
Invio dei dati
Ora che abbiamo compreso come gestire e validare l'input dell'utente, vediamo come passare il nuovo articolo all'applicazione per essere inserito nell'elenco degli articoli disponibili. Per questo passaggio possiamo rifarci a quanto abbiamo visto nelle puntate precedenti a proposito della generazione di eventi. Possiamo infatti intercettare l'evento click
sul pulsante di invio associando un metodo del nostro componente:
<form #myForm="ngForm">
...
<button type="button" [disabled]="!myForm.valid" (click)="inviaArticolo()">Invia</button>
</form>
Per rendere disponibile all'esterno l'articolo creato per mezzo del componente ArticoloFormComponent
, definiamo una proprietà di output submit
ed implementiamo il metodo inviaArticolo
in modo tale che generi uno specifico evento tramite EventEmitter
:
import { Component, Output, EventEmitter } from '@angular/core';
import { Articolo} from '../articolo';
@Component({
selector: 'articolo-form',
templateUrl: 'articolo-form.component.html',
styleUrls: ['articolo-form.component.css']
})
export class ArticoloFormComponent {
@Output() submit = new EventEmitter<Articolo>();
constructor() {
this.model = new Articolo();
}
inviaArticolo() {
this.submit.emit(this.model)
}
}
Quindi, ora nella nostra applicazione possiamo utilizzare il componente tramite il seguente markup:
<articolo-form (submit)="addArticolo($event)"></articolo-form>
Come possiamo vedere, abbiamo associato all'evento submit il gestore addArticolo()
la cui implementazione sarà simile a quanto mostrato di seguito:
addArticolo(articolo) {
this.elencoArticoli.push(articolo)
}
Per come abbiamo definito l'EventEmitter
associato alla proprietà di output submit
, il nuovo articolo definito tramite la form sarà disponibile come parametro al gestore dell'evento.