back to top

Reactive form in Angular

Dopo aver visto come creare dei form in Angular utilizzando il metodo Template-driven, vediamo ora un approccio alternativo. In questa lezione illustreremo cosa sono e come creare i cosiddetti Reactive form (o Model-driven Form). In questo caso svolgeremo gran parte del nostro lavoro nella classe che definisce un componente invece di procedere alla configurazione del form all’interno del template.

Come creare un Reactive form

Dopo aver usato l’approccio Template Driven, vediamo come creare un form simile a quello della lezione precedente, ma utilizzando i Reactive form.

La sola differenza in questo caso sarà l’aggiunta di un gruppo di campi di input per l’inserimento dell’indirizzo.

esempio struttura angular reactive form

Iniziamo quindi a creare un nuovo componente attraverso Angular CLI.

ng generate component reactive-form --no-spec
CREATE src/app/reactive-form/reactive-form.component.css (0 bytes)
CREATE src/app/reactive-form/reactive-form.component.html (33 bytes)
CREATE src/app/reactive-form/reactive-form.component.ts (303 bytes)
UPDATE src/app/app.module.ts (613 bytes)

Modifichiamo quindi il file app.module.ts e importiamo il modulo ReactiveFormsModule.

// file: app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; // 1

import { AppComponent } from './app.component';
import { 
  ReactiveFormComponent 
} from './reactive-form/reactive-form.component';

@NgModule({
  declarations: [
    AppComponent,
    ReactiveFormComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule // 2
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

A questo punto possiamo iniziare a creare il form semplice che arricchiremo in diverse fasi successive in modo tale da realizzare un form secondo l’approccio Model-driven.

<form novalidate>
  <div class="form-row">
    <div class="col-md-4 mb-2">
      <label for="name">Nome</label>
      <input type="text"
        class="form-control"
        id="name"
        name="name"
        placeholder="Nome">
    </div>
    <div class="col-md-4 mb-2">
      <label for="mail">E-mail</label>
      <input type="email"
        class="form-control"
        id="mail"
        name="mail"
        placeholder="Inserisci la tua e-mail">
    </div>
    <div class="col-md-4 mb-2">
      <label for="dayOfTheWeek">Giorno della settimana</label>
      <select
        name="dayOfTheWeek"
        id="dayOfTheWeek"
        class="form-control">
        <option 
          *ngFor="let day of daysOfTheWeek" 
          [value]="day">{{ day }}</option>
      </select>
    </div>
  </div>
  <div class="form-row">
    <div class="col-md-4 mb-2">
      <label for="address">Indirizzo</label>
      <input type="text"
        class="form-control"
        id="address"
        name="address"
        placeholder="Indirizzo">
    </div>
    <div class="col-md-4 mb-2">
      <label for="city">Citt&agrave;</label>
      <input type="text"
        class="form-control"
        id="city"
        name="city"
        placeholder="Citt&agrave;">
    </div>
    <div class="col-md-4 mb-2">
      <label for="zipCode">CAP</label>
      <input type="text"
        class="form-control"
        id="zipCode"
        name="zipCode"
        placeholder="CAP">
    </div>
  </div>
  <div class="form-row mt-3 mb-3">
    <div 
      class="form-check form-check-inline" 
      *ngFor="let office of availableOffices">
      <input type="radio"
        name="office"
        id="office-{{ office.id }}"
        class="form-check-input"
        value="{{ office.name }}">
      <label for="office" class="form-check-label">
        Ufficio {{ office.id }}
      </label>
    </div>
  </div>
  <div class="form-row mt-3 mb-3">
    <div 
      class="form-check form-check-inline" 
      *ngFor="let application of applications; let i = index;">
      <input type="checkbox"
        name="application{{ application.id }}"
        id="application-{{ application.id }}"
        class="form-check-input"
        value="richiesta_{{ application.id }}">
      <label for="application" class="form-check-label">
        Richiesta {{ application.id }}
      </label>
    </div>
  </div>
  <div class="form-row">
    <button
      type="submit"
      class="btn btn-primary">Prenota</button>
  </div>
</form>

In questo modo creiamo un form che ha gli stessi campi di quello presente nell’immagine riportata in alto. Per semplificare il processo di definizione e non essere ripetitivi, abbiamo utilizzato la direttiva *NgFor in diverse occasioni. Per questo motivo all’interno del componente abbiamo dichiarato tre proprietà come mostrato nel frammento di codice riportato di seguito.

import { Appointment } from '../appointment';
import { Component } from '@angular/core';

@Component({
  selector: 'simple-reactive-form',
  templateUrl: './reactive-form.component.html',
  styleUrls: ['./reactive-form.component.css']
})
export class ReactiveFormComponent {

  daysOfTheWeek = [
    'Lunedì',
    'Martedì',
    'Mercoledì',
    'Giovedì',
    'Venerdì'
  ];

  availableOffices = [
    {id: 'A', name: 'ufficio_A'},
    {id: 'B', name: 'ufficio_B'},
    {id: 'C', name: 'ufficio_C'},
  ];

  applications = [
    {id: 0},
    {id: 1},
    {id: 2}
  ];
}

Possiamo ora procedere a configurare il nostro componente in maniera opportuna al fine di realizzare un form che segua l’approccio Model-driven e per far ciò iniziamo a definire un oggetto di tipo FormGroup che rappresenti l’intero form. Per ciascuno dei campi di input creiamo quindi degli oggetti di tipo FormControl, così come mostrato nel frammento di codice riportato sotto.

//  ...
  import { FormGroup, FormControl } from '@angular/forms';

  //  ...

  export class ReactiveFormComponent {

  //  ...

  reactiveForm: FormGroup;

  constructor() {
    this.reactiveForm = new FormGroup({
      name: new FormControl(''),
      mail: new FormControl(''),
      dayOfTheWeek: new FormControl('Lunedì'),
      fullAddress: new FormGroup({
        address: new FormControl(''),
        city: new FormControl(''),
        zipCode: new FormControl('')
      }),
      office: new FormControl('ufficio_A'),
      applications: new FormGroup({
        application_0 : new FormControl(false),
        application_1: new FormControl(false),
        application_2: new FormControl(false)
      })
    });
  }

}

All’interno del costruttore del componente ReactiveFormComponent creiamo una nuova istanza di FormGroup che importiamo da @angular/forms. Questa costituisce il modello che deve essere associato al form presente nel template. Possiamo notare che per la maggior parte dei campi di input abbiamo creato un oggetto di tipo FormControl ad eccezione dei tre elementi relativi all’indirizzo che abbiamo deciso di raggruppare in un unico oggetto di tipo FormGroup annidato nel form. La stessa scelta è stata presa per i checkbox. Nel creare delle istanze di FormControl passiamo un argomento che rappresenta il valore iniziale del singolo campo.

Il passo successivo consiste nell’associare il modello appena creato nel componente con il form presente nel template. Per far ciò utilizzeremo tre direttive: FormGroup, FormGroupName e FormControlName che applicheremo sui diversi elementi come riportato nel frammento di codice sottostante.

<form novalidate [formGroup]="reactiveForm">  // FormGroup
  <div class="form-row">
    <div class="col-md-4 mb-2">
      <label for="name">Nome</label>
      <input type="text"
        formControlName="name" // FormControlName
        class="form-control"
        id="name"
        name="name"
        placeholder="Nome">
    </div>
    <div class="col-md-4 mb-2">
      <label for="mail">E-mail</label>
      <input type="email"
        formControlName="mail"  // FormControlName
        class="form-control"
        id="mail"
        name="mail"
        placeholder="Inserisci la tua e-mail">
    </div>
    <div class="col-md-4 mb-2">
      <label for="dayOfTheWeek">Giorno della settimana</label>
      <select
        formControlName="dayOfTheWeek"  // FormControlName
        name="dayOfTheWeek"
        id="dayOfTheWeek"
        class="form-control">
        <option 
          *ngFor="let day of daysOfTheWeek" 
          [value]="day">{{ day }}</option>
      </select>
    </div>
  </div>
  <div class="form-row" formGroupName="fullAddress">  // FormGroupName
    <div class="col-md-4 mb-2">
      <label for="address">Indirizzo</label>
      <input type="text"
        formControlName="address" // FormControlName
        class="form-control"
        id="address"
        name="address"
        placeholder="Indirizzo">
    </div>
    <div class="col-md-4 mb-2">
      <label for="city">Citt&agrave;</label>
      <input type="text"
        formControlName="city"  // FormControlName
        class="form-control"
        id="city"
        name="city"
        placeholder="Citt&agrave;">
    </div>
    <div class="col-md-4 mb-2">
      <label for="zipCode">CAP</label>
      <input type="text"
        formControlName="zipCode" // FormControlName
        class="form-control"
        id="zipCode"
        name="zipCode"
        placeholder="CAP">
    </div>
  </div>
  <div class="form-row mt-3 mb-3">
    <div 
      class="form-check form-check-inline" 
      *ngFor="let office of availableOffices">
      <input type="radio"
        formControlName="office"  // FormControlName
        name="office"
        id="office-{{ office.id }}"
        class="form-check-input"
        value="{{ office.name }}">
      <label 
        for="office" 
        class="form-check-label">
          Ufficio {{ office.id }}
        </label>
    </div>
  </div>
  <div class="form-row mt-3 mb-3">
    <div
      class="form-check form-check-inline"
      *ngFor="let application of applications; let i = index;"
      formGroupName="applications">  // FormGroupName
      <input type="checkbox"
        formControlName="application_{{ application.id }}"  // FormControlName
        name="application{{ application.id }}"
        id="application-{{ application.id }}"
        class="form-check-input"
        value="richiesta_{{ application.id }}">
      <label 
        for="application" 
        class="form-check-label">
          Richiesta {{ application.id }}
        </label>
    </div>
  </div>
  <div class="form-row">
    <button
      type="submit"
      class="btn btn-primary">Prenota</button>
  </div>
</form>

<br>

<pre><code>{{ reactiveForm.value | json }}</code></pre>

Nell’esempio mostrato la direttiva formGroup associa l’intero form all’istanza base di FormGroup di cui manteniamo un riferimento all’interno della proprietà reactiveForm del componente. Invece formControlName collega i singoli campi ai rispettivi oggetti di tipo FormControl. Per quanto riguarda invece i tre campi usati per l’indirizzo e per i checkbox, ricordiamo che abbiamo in precedenza creato delle istanze di tipo FormGroup annidate nell’oggetto base. Per questo motivo, prima di poter mappare i controlli contenuti nel gruppo, abbiamo utilizzato la direttiva formGroupName su un elemento. Indichiamo così che tutti i suoi discendenti, su cui è applicata la direttiva formControlName, fanno riferimento agli oggetti FormControl presenti nel gruppo annidato e non in quello base. Possiamo infine accedere ai valori correnti del form attraverso la proprietà value di reactiveForm che è l’istanza di FormGroup inizializzata nel costruttore del componente.

{
  "name": "",
  "mail": "",
  "dayOfTheWeek": "Lunedì",
  "fullAddress": {
    "address": "",
    "city": "",
    "zipCode": ""
  },
  "office": "ufficio_A",
  "applications": {
    "application_0": false,
    "application_1": false,
    "application_2": false
  }
}

Semplificare la configurazione di un form grazie alla classe FormBuilder

Come abbiamo avuto modo di vedere, definire un modello attraverso le classi FormGroup e FormControl comporta la scrittura di un blocco di codice che può risultare di difficile lettura, in particolar modo se creiamo dei form un po’ più complessi. Possiamo allora semplificare tale processo facendo uso della classe FormBuilder, importata sempre da @angular/forms, che consente di definire un oggetto di tipo FormGroup in maniera più semplice e attraverso una sintassi più sintetica.

Dovremo quindi importare FormBuilder da @angular/forms e iniettarlo nel costruttore. Possiamo poi procedere alla configurazione del form nel componente, all’interno del template invece non dovremo apportare nessuna modifica.

import { Appointment } from '../appointment';
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'simple-reactive-form',
  templateUrl: './reactive-form.component.html',
  styleUrls: ['./reactive-form.component.css']
})
export class ReactiveFormComponent {

  // ...

  reactiveForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {

    this.reactiveForm = this.formBuilder.group({
      name: [''],
      mail: [''],
      dayOfTheWeek: ['Lunedì'],
      fullAddress: this.formBuilder.group({
        address: [''],
        city: [''],
        zipCode: ['']
      }),
      office: ['ufficio_A'],
      applications: this.formBuilder.group({
        application_0 : [false],
        application_1 : [false],
        application_2 : [false]
      })
    });

  }

}

Dopo aver importato FormBuilder, utilizziamo il meccanismo di Dependency Injection di Angular per creare un’istanza formBuilder all’interno del componente. Attraverso quest’ultima possiamo definire la struttura del form avvalendoci del metodo formBuilder.group() che crea un nuovo oggetto di tipo FormGroup in cui definiamo varie proprietà di tipo FormControl. Come possiamo vedere, FormBuilder ci semplifica la vita, visto che sarà suo compito generare delle istanze di tipo FormControl in base ai parametri specificati nell’array che abbiamo assegnato a ciascuna proprietà. Abbiamo usato il primo elemento dell’array per indicare il valore iniziale di ciascun controllo.

Stato e validazione del form

Come già espresso nella precedente lezione, può essere necessario stabilire dei criteri di validazione per ciascun campo di input del form. Riprendendo l’esempio esaminato finora, possiamo utilizzare la configurazione creata con FormBuilder e importare la classe Validators da @angular/forms che contiene una serie di validatori predefiniti. Per semplicità mostreremo un esempio in cui stabiliamo delle regole di validazione solo per i due campi ‘Nome’ e ‘E-mail’

import { FormBuilder, Validators, FormGroup } from '@angular/forms';

// ... 

this.reactiveForm = this.formBuilder.group({
  name: ['',
    [
      Validators.required,
      Validators.minLength(2),
      Validators.maxLength(50)
    ]
  ],
  mail: ['',
    [
      Validators.required,
      Validators.pattern('[a-z0-9._%+-]+@[a-z0-9-.]+\.[a-z]{2,}$')
    ]
  ],
  dayOfTheWeek: ['Lunedì'],
  fullAddress: this.formBuilder.group({
    address: [''],
    city: [''],
    zipCode: ['']
  }),
  office: ['ufficio_A'],
  applications: this.formBuilder.group({
    application_0 : [false],
    application_1 : [false],
    application_2 : [false]
  })
});

Nel frammento di codice riportato sopra abbiamo definito dei criteri di validazione per i due campi ‘name’ e ‘mail’ aggiungendo un secondo elemento all’array assegnato a ciascuna proprietà. Nel caso siano necessari più validatori, come nell’esempio, questi vengono inseriti all’interno di un array.

Nel template possiamo poi visualizzare lo stato corrente dell’intero form.

<pre>
  <code>
  {{ reactiveForm.status }} // 'INVALID' o 'VALID'
  {{ reactiveForm.valid }}
  {{ reactiveForm.invalid }}
  {{ reactiveForm.pristine }}
  {{ reactiveForm.dirty }}
  {{ reactiveForm.touched }}
  {{ reactiveForm.untouched }}
  </code>
</pre>

Oppure ottenere delle informazioni sui singoli campi in modo del tutto simile a quanto visto nel caso dell’approccio Template-driven.

<pre>
  <!-- Proprietà del campo di input -->
  <!-- identificato dall'attributo formControlName="mail" -->
  dirty? {{ reactiveForm.controls.mail.dirty }}
  pristine? {{ reactiveForm.controls.mail.pristine }}
  touched? {{ reactiveForm.controls.mail.touched }}
  untouched? {{ reactiveForm.controls.mail.untouched }}
  valid? {{ reactiveForm.controls.mail.valid }}
  invalid? {{ reactiveForm.controls.mail.invalid }}
</pre>

All’interno del componente possiamo anche definire un riferimento ad un campo specifico e utilizzare nel template una sintassi più breve per visualizzare le stesse informazioni appena viste.

constructor(private formBuilder: FormBuilder) {

this.reactiveForm = this.formBuilder.group({
  name: [''],
  mail: [''],
});

this.mailControl = this.reactiveForm.get('mail');
<pre>
  <!-- Proprietà del campo di input -->
  <!-- identificato dall'attributo formControlName="mail" -->
  dirty? {{ mailControl.dirty }}
  pristine? {{ mailControl.pristine }}
  touched? {{ mailControl.touched }}
  untouched? {{ mailControl.untouched }}
  valid? {{ mailControl.valid }}
  invalid? {{ mailControl.invalid }}
</pre>

Allo stesso modo, è possibile cambiare l’aspetto dei campi di input e mostrare all’utente un messaggio di feedback.

<div class="col-md-4 mb-2">
  <label for="mail">E-mail</label>
  <input type="email"
    formControlName="mail"
    class="form-control"
    [ngClass]= "{
      'is-invalid': mailControl.invalid && mailControl.dirty,
      'is-valid': mailControl.valid && mailControl.dirty
    }"
    id="mail"
    name="mail"
    placeholder="Inserisci la tua e-mail">
  <div
    *ngIf="mailControl.errors && mailControl.dirty"
    class="invalid-feedback">
    <p *ngIf="mailControl.errors.pattern" >
      Formato e-mail non valido
    </p>
    <p *ngIf="mailControl.errors.required" >
      * Campo obbligatorio
    </p>
  </div>
</div>

E proprio come accade per i form in stile Template-driven è consentito disabilitare il pulsante del form, attivarlo solo quando quest’ultimo è valido e gestire l’invio del modulo con NgSubmit.

<form novalidate [formGroup]="reactiveForm" (ngSubmit)="submitForm()">
  <div class="form-row">
    <div class="col-md-4 mb-2">
      <label for="name">Nome</label>
      <input type="text"
        formControlName="name"
        class="form-control"
        id="name"
        name="name"
        placeholder="Nome">
    </div>
    <div class="col-md-4 mb-2">
      <label for="mail">E-mail</label>
      <input type="email"
        formControlName="mail"
        class="form-control"
        [ngClass]= "{
          'is-invalid': mailControl.invalid && mailControl.dirty,
          'is-valid': mailControl.valid && mailControl.dirty
        }"
        id="mail"
        name="mail"
        placeholder="Inserisci la tua e-mail">
      <div
        *ngIf="mailControl.errors && mailControl.dirty"
        class="invalid-feedback">
        <p *ngIf="mailControl.errors.pattern" >
          Formato e-mail non valido
        </p>
        <p *ngIf="mailControl.errors.required" >
          * Campo obbligatorio
        </p>
      </div>
    </div>
  </div>
  <div class="form-row">
    <button
      type="submit"
      class="btn "
      [ngClass]="{
        'btn-secondary': reactiveForm.invalid,
        'btn-success': reactiveForm.valid
      }"
      [disabled]="reactiveForm.invalid">Prenota</button>
      <button
      type="submit"
      class="btn btn-danger"
      (click)="reactiveForm.reset()"
      [disabled]="reactiveForm.invalid">Reset</button>
  </div>
</form>

Come aggiungere dei campi ad un form in modo dinamico

Per concludere questa lezione illustriamo un altro semplice esempio in cui useremo un’istanza di FormArray che importiamo da @angular/forms per aggiungere dei campi di input in maniera dinamica.

Nel caso specifico vogliamo realizzare un form che permetta di salvare delle ricette di cucina. Per ciascuna ricetta utilizziamo un campo di tipo testo per ogni ingrediente. Per consentire l’immissione di un numero non predefinito di ingredienti, inseriamo un pulsante che crea dei nuovi campi di input.

esempio reactive form con FormArray

Per prima cosa, dopo aver creato i file necessari con Angular CLI, iniziamo a modificare il codice del componente come mostrato nel frammento di codice sottostante.

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, FormArray } from '@angular/forms';

@Component({
  selector: 'simple-reactive-form-2',
  templateUrl: './reactive-form2.component.html',
  styleUrls: ['./reactive-form2.component.css']
})
export class ReactiveForm2Component {

  reactiveForm: FormGroup;
  ingredients: FormArray;

  constructor(private formBuilder: FormBuilder) {

    this.reactiveForm = this.formBuilder.group({
      title: [''],
      ingredients: this.formBuilder.array([
        this.formBuilder.control('')
      ])
    });

    this.ingredients = <FormArray>this.reactiveForm.get('ingredients');

  }

  addIngredient() {
    this.ingredients.push(this.formBuilder.control(''));
  }

}

Iniettiamo nel costruttore del nostro componente un’istanza di FormBuilder con cui definiamo il modello di un semplice form composto da un campo di input per il titolo della ricetta e un’istanza di FormArray, associata alla proprietà ingredients, che consentirà di aggiungere in maniera dinamica dei nuovi input di testo per gli ingredienti. (Inizialmente è possibile inserire un solo ingrediente) Otteniamo quindi un riferimento all’istanza di FormArray che assegniamo alla proprietà del componente ingredients. In questo modo sarà più semplice accedere alle informazioni nel template. Il metodo FormGroup.get() restituisce in generale un oggetto di tipo AbstractControl o null. Per questo motivo, attraverso un’asserzione di tipo (<FormArray>), indichiamo al compilatore che in questo caso specifico sarà assegnato alla proprietà ingredients un oggetto di tipo FormArray. Infine abbiamo definito un metodo addIngredient() che invocheremo ogni volta che viene cliccato il pulsante per aggiungere un nuovo campo di testo. Quando ciò avviene, inseriamo nell’istanza di FormArray un nuovo oggetto di tipo FormControl utilizzando il metodo push().

<form novalidate [formGroup]="reactiveForm"> <!-- formGroup -->
  <div class="form-row">
    <div class="col-md-6 mb-2">
      <label for="title">Titolo</label>
      <!-- formControlName -->
      <input type="text"
        formControlName="title"
        class="form-control"
        id="title"
        name="title"
        placeholder="Titolo ricetta">
    </div>
  </div>
  <fieldset formArrayName="ingredients"> <!-- formArrayName -->
    <legend>Ingredienti</legend>
    <div 
      class="form-row" 
      *ngFor="let ingredient of ingredients.controls; let i=index" >
      <div class="col-md-6 mb-2">
        <!-- formControlName -->
        <input type="text"
        class="form-control"
        id="ingredient-{{ i }}"
        name="ingredient-{{ i }}"
        placeholder="Nuovo ingrediente"
        [formControlName]="i">
      </div>
    </div>
  </fieldset>
  <div class="form-row">
    <button 
      class="btn btn-primary" 
      (click)="addIngredient()">
        Aggiungi nuovo ingrediente
    </button>
  </div>
</form>

<pre><code>{{ reactiveForm.value | json }}</code></pre>

Per completare il nostro esempio, abbiamo mappato il modello creato nel componente all’interno del template grazie alle direttive FormGroup, per associare l’intero form all’istanza reactiveForm, e formArrayName, per collegare gli elementi di input del form all’istanza di FormArray creata nel componente. Per la direttiva *NgFor utilizziamo l’istanza di FormArray (ingredients) che avevamo ottenuto grazie al metodo reactiveForm.get().

Riepilogo

In questa lezione abbiamo illustrato come creare un form utilizzando un approccio alternativo ai Template-driven form, ovvero realizzando dei moduli, che vengono definiti Reactive form o Model driven form, in cui il codice per la configurazione e la validazione dei form viene in gran parte spostato dal template alla classe che definisce il componente. Abbiamo visto come sia possibile semplificare la fase iniziale di setup del form grazie alla classe FormBuilder e descritto in che modo stabilire delle regole per la validazione dei campi. Infine abbiamo mostrato come creare dei form dinamici che permettono di aggiungere un numero non predeterminato di campi. Nella prossima lezione introdurremo la libreria RxJS che viene spesso utilizzata in Angular e illustreremo i concetti basilari che consentiranno di capire meglio gli argomenti che tratteremo nelle lezioni successive.

Pubblicitร