Un confronto tra React, Angular, Vue.js e Svelte: Form e validazione

di Morgan Pizzini, in HTML5,

Negli articoli precedenti abbiamo confrontato tra di loro i principali framework e le librerie per la creazione di applicazioni di tipo Single Page Application (SPA). In questo articolo concludiamo il confronto aggiungendo il punto di contatto tra i componenti e le chiamate HTTP verso il backend analizzando la struttura dei componenti per costruire le form e la corretta implementazione per applicare regole di validazione.

Benché inutile a livello applicativo, dato che la vera validazione avverrà lato server, è buona norma informare il prima possibile l'utente riguardo a eventuali errori di compilazione. Questo faciliterà l'interazione con il sito web e ridurrà di conseguenza le chiamate verso il server nel momento in cui il primo livello di validazione risulti errato. Questo porta a ovvi vantaggi non solo per l'usabilita, ma anche per le performance dell'intero sistema.

L'applicazione di una corretta validazione sta alla base di ogni buon form, ma ogni framework ne ha ovviamente una sua implementazione che può essere più o meno elastica. In questo articolo andremo a mostrare degli esempi che forniscono un ottimo punto di partenza che potrà poi essere esteso e integrato per casistiche personali.

Angular

Ormai l'abbiamo imparato: qualsiasi implementazione Angular prevede la scrittura di un codice più articolato rispetto gli altri competitor. Questo è si un punto a favore dell'architettura finale del progetto, ma allo stesso tempo può richiedere più tempo per l'adozione. Inoltre per Angular troviamo due strutture diverse per mostrare lo stesso layout: FormsModule e ReactiveFormsModule. Questi moduli sono due facce della stessa moneta e permettono di creare un form con validazioni anche con modalità diametralmente opposte. A seconda della nostra necessità dovremmo quindi importare il giusto modulo all'interno del modulo dell'app.

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
  imports:      [ FormsModule, ReactiveFormsModule ],
  [...]
})
export class AppModule { }
FormsModule

La caratteristica principale del FormsModule è il fatte che la dichiarazione del form dovrà essere effettuata nel codice HTML del componente tramite l'utilizzo di apposite direttive di Angular.

<form (ngSubmit)="onSubmit()" #myForm="ngForm">
  <button type="submit" class="btn btn-success" [disabled]="!myForm.form.valid">
    Submit
  </button>
  <button type="button" class="btn btn-default" (click)="myForm.reset()">
    Reset
  </button>
</form>

La direttiva genererà un oggetto attraverso il quale potremo sapere lo stato attuale del form o resettarne i valori. A questo basterà aggiungere i vari campi input rispettando la seguente struttura

<label for="name">Name</label>
<input type="text" id="name" [(ngModel)]="model.name" name="name" #name="ngModel" required/>
<div [hidden]="name.valid || name.pristine">
  Name is required
</div>

Il codice appare molto simile a quello HTML a parte due attributi che verranno utlizzati da Angular per creare deli riferimenti all'interno del componente. ngModel specifica il modello che dovrà essere applicato sull'elemento, dove model è un oggetto dichiarato nella parte TypeScript del componente. La presenza delle parentesi predispone un binding in due direzioni: "[]" per copiare il valore dal codice TypeScript a quello HTML e "()" per aggiornare il modello a seguito di un input dell'utente. #name="ngModel" al pari di ngForm sarà utilizzato per definire una variabile HTML che potrà essere utilizzata dal div sottostante per controllare la validazione. Validazione che applicherà delle classi predefinite che potremmo utilizzare per applicare il nostro stile.

.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
}

Angular predispone anche alcuni stati per ogni elemento che potremmo utilizzare a seconda delle necessità: nel caso in cui il form sia stato toccato abbiamo touched e untouched, se il valore è stato modificato troveremo dirty o pristine. Questi stati verranno riflessi sui tag HTML tramite aplicazioni di classi con prefisso "ng-".

Per l'applicazione di regole di validazione più complesse, dobbiamo affidarci alle direttive. Scriviamo un esempio che permetterà di emettere un errore nel caso venga inserito un nome non consentito.

import { Component, Directive, Input } from '@angular/core';
import { ValidationErrors, ValidatorFn, AbstractControl, NG_VALIDATORS, Validator } from '@angular/forms';

@Directive({
  selector: '[appForbiddenName]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
  @Input('appForbiddenName') forbiddenName: string;

  validate(control: AbstractControl): {[key: string]: any} {
    return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
                              : null;
  }
}

// app.module.ts
import { ForbiddenValidatorDirective } from './directives/app.component';
@NgModule({
  [..]
  declarations: [ ForbiddenValidatorDirective ]
})
export class AppModule { }

Per applicarlo al nostro form non dovremmo far altro che seguire la dichiarazione seguente, aggiornando anche la logica di validazione.

<label for="name">Name</label>
<input type="text" id="name" [(ngModel)]="model.name" name="name" #name="ngModel"  
  minlength="4"
  appForbiddenName="morgan"
  required/>
<div *ngIf="name.invalid && (name.dirty || name.touched)">
  <div *ngIf="name.errors?.required">Nome richiesto.</div>
  <div *ngIf="name.errors?.minlength">
    Il nome dovrà essere di almeno 4 caratteri
  </div>
  <div *ngIf="name.errors?.forbiddenName">Morgan non valido.</div>
</div>
Reactive Forms module

Con l'utilizzo del ReactiveFormsModule, la procedura di inizializzazione e setup si sposta dal codice HTML a quello TypeScript, utilizzando classi e metodi per la creazione di un oggetto di tipo FormGroup

.
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  myForm: FormGroup;

  get name() {
    return this.myForm.get('name');
  }

  constructor() {
    this.myForm = new FormGroup({
      name: new FormControl(this.model.name, [Validators.required]),
    });
  }

  onSubmit() {}
}

Vi è in realtà un'altra modalità per l'inizializzazione che consiste nell'iniettare un istanda di FormBuilder e utilizzarne i metodi. Il codice che usa FormBuilder è più compatto ed il risultato è coincidente col precedente. Come possiamo notare oltre alla definizione del form e al valore iniziale del controllo name, abbiamo creato una proprietà get-only che ci tornerà utile nel codice HTML per valorizzare l'input.

<form (ngSubmit)="onSubmit()" [formGroup]="myForm">
  <div class="form-group">
    <label for="name">Name</label>
    <input
      type="text"
      class="form-control"
      id="name"
      name="name"
      formControlName="name"
    />
    <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
      Name is required
    </div>
  </div>
  <button type="submit" class="btn btn-success" [disabled]="!myForm.valid">
    Submit
  </button>
  <button type="button" class="btn btn-default" (click)="myForm.reset()">
    Reset
  </button>
</form>

Come notiamo il codice HTML è molto più leggero del precedente. Utilizzando solo dei puntatori come formGroup e formControlName possiamo mantenere un'alta leggibilità e concentrare gli sviluppi o validazioni all'interno del TypeScript.

Per definire altre validazioni possiamo creare una funzione che restituise un oggetto ValidatorFn che utilizzeremo in fase di creazione del form.

export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

[...]

this.myForm = new FormGroup({
  name: new FormControl(this.model.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/Morgan/i)
  ])
});

Come per il FormsModule, possiamo intercettare ogni singolo errore di validazione lato HTML.

<div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert">
  <div *ngIf="name.errors?.required">Name è richiesto.</div>
  <div *ngIf="name.errors?.minlength">
    Il nome deve avere almeno 4 caratteri
  </div>
  <div *ngIf="name.errors?.forbiddenName">
    Name non può essere Morgan
  </div>
</div>

React

Abbandoniamo ora l'ecosistema Angular, con tutta la sua infrastruttura, per dedicarci agli altri framework. Per ognuno vi è la possibilità di impostare una validazione attraverso librerie ad-hoc, ma per gli scopi di questo articolo tenteremo di creare una struttura il più semplice possibile, dalla quale è poi possibile integrare altre funzionalità.

Il punto di partenza di ogni form è la validazione, per questa ci affidiamo a funzioni che possono essere dichiarate all'interno di qualunque JavaScript comune, e le andiamo poi ad inglobare all'interno di una variabile validate.

const commonValidation = (nomeCampo, valoreCampo) => {
  if (valoreCampo.trim() === '') {
    return `${nomeCampo} è richiesto`;
  }
  if (/[^a-zA-Z -]/.test(valoreCampo)) {
    return 'Caratteri non validi';
  }
  if (valoreCampo.trim().length < 3) {
    return `${nomeCampo} deve essere almeno di 3 caratteri`;
  }
  return null;
};

const validate = {
  firstName: name => commonValidation('Nome', name),
  lastName: name => commonValidation('Cognome', name)
};

Creiamo ora il codice HTMLl che ci permetterà di mostrare un form e interagire con le validazioni.

<form onSubmit={handleSubmit}>
  <div className="form-group">
    <label htmlFor="first-name-input">
      First Name *
      <input
        type="text"
        className="form-control"
        id="first-name-input"
        placeholder="Enter first name"
        value={values.firstName}
        onChange={handleChange}
        onBlur={handleBlur}
        name="firstName"
        required
      />
      {touched.firstName && errors.firstName}
    </label>
  </div>
  <div className="form-group">
    <label htmlFor="last-name-input">
      Last name *
      <input
        type="text"
        className="form-control"
        id="last-name-input"
        placeholder="Enter last name"
        value={values.lastName}
        onChange={handleChange}
        onBlur={handleBlur}
        name="lastName"
        required
      />
      {touched.lastName && errors.lastName}
    </label>
  </div>
  <div className="form-group">
    <button type="submit" className="btn btn-primary">
      Submit
    </button>
  </div>
</form>

Nell'elemento form inseriamo l'attributo che ci permetterà di intercettare il click sul bottone di submit, all'interno di ogni elemento input inseriamo il valore e gli eventi per gestire il cambiamento e la perdita del focus. Come ultima operazione inseriamo gli errori, che verranno visualizzati solamente se l'input associato è stato toccato.

Come prima fase attraverso lo state di React creiamo delle variabili per valori, errori e per ricordarci se il controllo è stato toccato, e aggiungiamo gli EventHandlers per cambio valore e perdita focus.

  const [values, setValues] = React.useState({ ...valori iniziali... });
  const [errors, setErrors] = React.useState({});
  const [touched, setTouched] = React.useState({});

  // controllo cambiamento input
  const handleChange = evt => {
    // estraggo i dati dall'evento
    const { name, value: newValue, type } = evt.target;

    // eventuale conversione da stringa a numero
    const value = type === 'number' ? +newValue : newValue;

    // aggiornamento dello state
    setValues({
      ...values,
      [name]: value,
    });

    // aggiornamento tocco sull'input
    setTouched({
      ...touched,
      [name]: true,
    });
  };

  const handleBlur = evt => {
    const { name, value } = evt.target;

    // dagli errori precedenti rimuovo quelli sull'input corrente, dato che verrà ri-validato
    const { [name]: removedError, ...rest } = errors;

    // validazione dell'input
    const error = validate[name](value);

    // Inserimento degli errori se presenti e se il controllo è stato toccato
    setErrors({
      ...rest,
      ...(error && { [name]: touched[name] && error }),
    });
  };

L'ultimo step, e forse quello più complesso in termini di codice, è la gestione del submit nel quale, attraverso il metodo reduce, estraiamo tutti gli errori e i tocchi sull'input e analiziamo la possibilità di proseguire, oppure emettere un errore.

const handleSubmit = evt => {
  evt.preventDefault();

  // validazione form
  const formValidation = Object.keys(values).reduce(
    (acc, key) => {
      const newError = validate[key](values[key]);
      const newTouched = { [key]: true };
      return {
        errors: {
          ...acc.errors,
          ...(newError && { [key]: newError }),
        },
        touched: {
          ...acc.touched,
          ...newTouched,
        },
      };
    },
    {
      errors: { ...errors },
      touched: { ...touched },
    },
  );
  // aggiornamento stato errori e tocchi
  setErrors(formValidation.errors);
  setTouched(formValidation.touched);

  if (
    !Object.values(formValidation.errors).length && // la form contiene degli errori
    Object.values(formValidation.touched).length ===
      Object.values(values).length && // i valori estratti sono uguali al numero di input toccati
    Object.values(formValidation.touched).every(t => t === true) // tutti gli input sono stati toccati
  ) {
    alert("Errore");
  }
};

Come detto questo codice può essere semplificato attraverso l'utilizzo di varie librerie create appositamente per eseguire questi controlli al posto nostro, ad esempio Formik

.
2 pagine in totale: 1 2
Contenuti dell'articolo

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

Top Ten Articoli

Articoli via e-mail

Iscriviti alla nostra newsletter nuoviarticoli per ricevere via e-mail le notifiche!

In primo piano

I più letti di oggi

In evidenza

Misc