Dall'archivio articoli > HTML5
Un confronto tra React, Angular, Vue.js e Svelte: Form e validazione
Per poter utilizzare questa funzionalità, devi fare il login o iscriverti.
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.
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 { }
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>
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>
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
.Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.