Angular ReactiveForms e Custom Async Validators

Home/angular, code, italian, javascript/Angular ReactiveForms e Custom Async Validators

Angular ReactiveForms e Custom Async Validators

In questo articolo descrivo la creazione di un validator custom asincrono da utilizzare nei Reactive Forms inclusi nell’ultima release del framework Angular.
Nello specifico, andremo a realizzare un form per l’inserimento di un nuovo utente, creando un validatore ad-hoc per verificare la presenza di tale utente su un database attraverso l’utilizzo di un servizio REST, che restituirà un errore, bloccando inoltre la possibilità di effettuare il submit del form, nel caso fosse già presente.

Angular Form

Angular include nel proprio framework due tipologie di form:

  • Template driven-form: che al contrario di quanto molti pensino sono davvero molto potenti e permettono di realizzare form complessi senza alcuna riga di codice Javascript (o quasi), semplicemente inserendo nel template HTML direttive built-in e/o custom.
  • Reactive Forms: oggetto di questo articolo, che forniscono invece una notevole flessibilità rispetto ai primi e sono più semplici da debuggare e testare: ad esempio, è possibile modificare validatori a runtime, creare dinamicamente form a partire da una configurazione, suddividere un form in diversi componenti e gruppi”, e molto altro

Su entrambe le tipologie di form esistono moltissime risorse e documentazione. Tuttavia, ho trovato diversi esempi sulla creazione di validatori sincroni (anche nella documentazione ufficiale) ma scarso materiale, invece, sui validatori custom asincroni nel caso di Reactive Forms. Ho pensato quindi di scrivere questo articolo presentando un esempio molto semplice.

Creare un form reattivo

1) Per creare un form reattivo, è necessario innanzitutto inserire un form e un campo di input all’interno di un template HTML. A tal proposito utilizziamo solo un paio degli strumenti forniti dal framework per gestire questa tipologia di form: formGroup e formControlName

L’obiettivo finale dell’articolo sarà quello di creare un validatore custom per verificare se il nome utente è già presente su un database

<form [formGroup]="form" (submit)="save()">
  <input 
    type="text" 
    formControlName="name"
    placeholder="Search a free username (min 3 chars)"
  >

  <button [disabled]="form.invalid">
    Send
  </button>
</form>

2) È infine necessario inizializzare il form e i vari controlli. Per farlo utilizzeremo una delle utility fornite dal framework, ovvero FormBuilder:

  • usiamo quindi FormBuilder per inizializzare il form.
  • viene inizializzato il campo name per il quale definiamo il valore di default, null, come primo parametro, e un paio di validatori built-in, forniti in Angular (required e minlength), come secondo parametro.
  • Oltre ai validatori inclusi in Angular, potremmo aggiungere anche dei validatori custom sincroni, che tuttavia sono molto semplici da realizzare e non sono oggetto di questo articolo
  • il metodo save() sarà quindi invocato al submit e il pulsante inserito nel template sarà disabilitato fino a che tutti i campi del form non saranno appunto validi (in questo caso solo uno per non complicare troppo il tutorial e focalizzarci sul validatore)
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
})
export class AppComponent  {
  form: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      'name': [
        // initial value
        null,
        // built-in validators
        Validators.compose(
          [ Validators.required, Validators.minLength(3), anyCustomSyncValidator ],
        )
      ],
    });
  }

  save() {
    console.log('save to db');
  }
}

Introduzioni ai custom AsyncValidator

Per creare un async validator da utilizzare nei Reactive Form è necessario creare una semplice funzione che restituisca un oggetto di tipo AsyncValidatorFn. Dal source code di Angular possiamo verificare che tale tipo è rappresentato da una Promise o da un Observable di ValidationErrors, che di seguito ricopio:

export interface AsyncValidatorFn {
    (c: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
}
export declare type ValidationErrors = {
    [key: string]: any;
};

Quindi, nel nostro caso, se volessimo creare uno UserValidator che verifichi la presenza di un utente su un database, potremmo creare una funzione come la seguente:

Il servizio HttpClient fornito da Angular restituisce un Observable e utilizziamo l’operatore map per trasformare il risultato nell’oggetto richiesto dall’interfaccia del validatore

userValidator(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<{ [key: string]: any } | null> => {
      return this.http.get(URL_API?user=YOUR_USERNAME)
        .pipe(
          map(res => {
            // if username is taken
            if (CONDITION) {
              // return error (key: value)
              return { 'customErrorName': true};
            }
          })
        );
    };

Creazione del validatore userValidator

Tra i vari possibili approcci che possiamo utilizzare per la creazione di un validatore riutilizzabile, dato che in Angular abbiamo la possibilità di sfruttare il motore di Dependency Injection, ho scelto la creazione di un servizio.

Di seguito creiamo quindi un service che interroga un’API gratuita (jsonplaceholder) per verificare se l’utente è già presente sul suo database o meno (in realtà è un JSON ma per questo esempio è più che sufficiente).
Nel caso l’utente fosse già stato inserito, ci verrà restituito un array con un elemento. In caso negativo, un array vuoto.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms';
import { Observable, timer } from 'rxjs';
import { map, switchMap  } from 'rxjs/operators';

export const URL = 'https://jsonplaceholder.typicode.com';

@Injectable({
  providedIn: 'root'
})
export class UserValidators {
  constructor(private http: HttpClient) {}

  userValidator(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<{ [key: string]: any } | null> => {
      return this.http.get<any>(`${URL}/users?username=${control.value}`)
        .pipe(
          map(res => {
            // return a custom error if username is taken (result is an array with one or more elements), 
            // otherwise return no error if the array is empty
            return (res.length) ? { 'userNameExists': true} : null
            }
          })
        );
    };
  }
}

Analizziamo brevemente lo script precedente:

  • La classe utilizza la funzionalità providedIn del decorator Injectable, disponibile da Angular 6, per definire il provider nel modulo root senza la necessità di doverlo specificare manualmente nel modulo
  • Il metodo userValidator sarà invocato ogni volta che un utente digiterà un carattere nel campo di input ed effettuerà la chiamata ad un ipotetico servizio REST per la validazione dello username ottenuto dal campo di input(control.value)
  • Utilizziamo l’operatore map di RxJS per trasformare il risultato in un oggetto che rappresenti il tipo di errore da utilizzare nella UI per visualizzare un messaggio di errore ad hoc

Utilizzare il validatore

Come utilizzare questo validator asincrono? Aggiorniamo il codice in cui abbiamo inizializzato il form, iniettiamo il nuovo servizio e inseriamo il validatore come terzo parametro del metodo group(), utilizzato proprio per l’inserimento di validatori asincroni.

import { Component } from '@angular/core';
import { 
  FormBuilder, FormControl, FormGroup, Validators 
} from '@angular/forms';
import { UserValidators } from './validators/user.validator';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
})
export class AppComponent  {
  form: FormGroup;

  constructor(
    private fb: FormBuilder,
    private service: UserValidators
  ) { }

  ngOnInit() {
    this.form = this.fb.group({
      'name': [
        // initial value
        null,
        // sync built-in validators
        Validators.compose(
          [ Validators.required, Validators.minLength(3) ],
        ),
        // custom async validator
        this.service.userValidator()
      ],
    });
  }

  save() {
    console.log('save to db');
  }
}

Il template HTML ora sarà in grado di gestire:

  • la proprietà pending, per mostrare eventuali loader/spinner/testi durante la fase di caricamento
  • una nuova tipologia di errore, userNameExists, nel caso di username non disponibile
<form [formGroup]="form" (submit)="save()">
  <p>Find a free username</p>

  <input 
    style="width: 220px"
    type="text" 
    formControlName="name"
    placeholder="Search a free username (min 3 chars)"
  >

  <button [disabled]="form.invalid || form.pending">
    Send
    <span *ngIf="form.get('name').pending"> (searching)</span>
  </button>

  <p 
    style="color: red"
    *ngIf="form.get('name').errors?.userNameExists">Not available</p>
</form>

Debounce

L’attuale validatore ha un grosso difetto: viene invocato dopo la digitazione di ogni carattere, mentre sarebbe più opportuno applicare un debounce per effettuare la validazione solo TOT millisecondi dopo il termine dell’inserimento.

Potremmo “wrappare” la nostra chiamata HTTP in un setTimeout allo scopo di ritardare la richiesta applicando un delay e distruggendo il timer (clearInterval) ad ogni digitazione.
Bene…. ma non benissimo 🙂

Potremmo invece utilizzare l’operatore debounceTime(value) fornito da RxJS. Tuttavia non è possibile farlo all’interno del validatore ma dovremmo applicarlo in fase di inizializzazione del nostro form (ad es. sfruttando la proprietà valueChanges dei controlli), quindi nel componente che lo definisce.
Bello… ma non stupendo. Se volessimo infatti riutilizzare questo validatore dovremmo duplicare questa procedura in ogni form e scrivere un bel po’ di codice in più.

Un modo più “elegante” e flessibile è invece quello di inserire questa logica all’interno del validatore utilizzando l’operatore timer associato ad un switchMap, come vedrete di seguito.
Tuttavia, per evitare un codice poco leggibile, ho preferito dividere in due differenti metodi il validatore, come vedrete di seguito:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms';
import { Observable, timer } from 'rxjs';
import { map, switchMap  } from 'rxjs/operators';

export const URL = 'https://jsonplaceholder.typicode.com';

@Injectable({
  providedIn: 'root'
})
export class UserValidators {
  constructor(private http: HttpClient) {}

  searchUser(text) {
    // debounce
    return timer(1000)
      .pipe(
        switchMap(() => {
          // Check if username is available
          return this.http.get<any>(`${URL}/users?username=${text}`)
        })
      );
  }

  userValidator(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<{ [key: string]: any } | null> => {
      return this.searchUser(control.value)
        .pipe(
          map(res => {
            // if username is already taken
            if (res.length) {
              // return error
              return { 'userNameExists': true};
            }
          })
        );
    };

  }
}

DEMO

Il codice completo e funzionante è disponibile su StackBlitz:

2018-07-15T15:56:08+00:00 luglio 15th, 2018|angular, code, italian, javascript|

Leave A Comment