Lavorare con Angular 4 Form: nidificazione e convalida dell'input
Pubblicato: 2022-03-11Sul Web, alcuni dei primi elementi di input dell'utente erano un pulsante, una casella di controllo, un input di testo e pulsanti di opzione. Fino ad oggi, questi elementi sono ancora utilizzati nelle moderne applicazioni web anche se lo standard HTML ha fatto molta strada dalla sua definizione iniziale e ora consente ogni tipo di interazione fantasiosa.
La convalida degli input degli utenti è una parte essenziale di qualsiasi applicazione Web robusta.
I moduli nelle applicazioni angolari possono aggregare lo stato di tutti gli input che si trovano in quel modulo e fornire uno stato generale come lo stato di convalida del modulo completo. Questo può tornare molto utile per decidere se l'input dell'utente verrà accettato o rifiutato senza controllare ogni input separatamente.
In questo articolo imparerai come lavorare con i moduli ed eseguire facilmente la convalida dei moduli nella tua applicazione Angular.
In Angular 4 sono disponibili due diversi tipi di moduli con cui lavorare: moduli basati su modelli e moduli reattivi. Analizzeremo ogni tipo di modulo usando lo stesso esempio per vedere come le stesse cose possono essere implementate in modi diversi. Più avanti, nell'articolo, esamineremo un nuovo approccio su come impostare e lavorare con i moduli nidificati.
4 forme angolari
In Angular 4, i seguenti quattro stati sono comunemente usati dai moduli:
valid – stato di validità di tutti i controlli del modulo, true se tutti i controlli sono validi
non valido – inverso di
valid; true se alcuni controlli non sono validipristine: fornisce uno stato sulla "pulizia" della forma; true se nessun controllo è stato modificato
sporco – inverso di
pristine; true se alcuni controlli sono stati modificati
Diamo un'occhiata a un esempio di base di un modulo:
<form> <div> <label>Name</label> <input type="text" name="name"/> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear"/> </div> <div> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country"/> </div> <div> <label>City</label> <input type="text" name="city"/> </div> </div> <div> <h3>Phone numbers</h3> <div> <label>Phone number 1</label> <input type="text" name="phoneNumber[1]"/> <button type="button">remove</button> </div> <button type="button">Add phone number</button> </div> <button type="submit">Register</button> <button type="button">Print to console</button> </form>La specifica per questo esempio è la seguente:
nome - è obbligatorio e unico tra tutti gli utenti registrati
anno di nascita - deve essere un numero valido e l'utente deve avere almeno 18 anni e meno di 85 anni
country - è obbligatorio, e solo per rendere le cose un po' complicate, abbiamo bisogno di una convalida che se il paese è la Francia, la città deve essere Parigi (diciamo che il nostro servizio è offerto solo a Parigi)
phoneNumber: ogni numero di telefono deve seguire uno schema specifico, deve esserci almeno un numero di telefono e l'utente può aggiungere un nuovo numero o rimuovere un numero di telefono esistente.
Il pulsante “Registrati” è abilitato solo se tutti gli input sono validi e, una volta cliccato, invia il form.
La "Stampa su console" stampa semplicemente il valore di tutti gli input sulla console quando si fa clic.
L'obiettivo finale è implementare completamente la specifica definita.
Moduli basati su modelli
I moduli basati su modelli sono molto simili ai moduli in AngularJS (o Angular 1, come alcuni lo chiamano). Quindi, qualcuno che ha lavorato con i moduli in AngularJS avrà molta familiarità con questo approccio al lavoro con i moduli.
Con l'introduzione dei moduli in Angular 4, viene imposto che ogni tipo specifico di modulo si trovi in un modulo separato e dobbiamo definire esplicitamente quale tipo utilizzeremo importando il modulo appropriato. Quel modulo per i moduli basati su modello è FormsModule. Detto questo, puoi attivare i moduli basati su modelli come segue:
import {FormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}Come presentato in questo frammento di codice, dobbiamo prima importare il modulo browser poiché "fornisce servizi essenziali per avviare ed eseguire un'app browser". (dai documenti di Angular 4). Quindi importiamo il FormsModule richiesto per attivare i moduli basati su modelli. E l'ultima è la dichiarazione del componente root, AppComponent, dove nei prossimi passi implementeremo il form.
Tieni presente che in questo esempio e negli esempi seguenti devi assicurarti che l'app venga avviata correttamente utilizzando il metodo platformBrowserDynamic .
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {AppModule} from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);Possiamo presumere che il nostro AppComponent (app.component.ts) assomigli a questo:
import {Component} from '@angular/core' @Component({ selector: 'my-app', templateUrl: 'src/app.component.tpl.html' }) export class AppComponent { }Dove il modello di questo componente si trova in app.component.tpl.html e possiamo copiare il modello iniziale in questo file.
Si noti che ogni elemento di input deve avere l'attributo name per essere identificato correttamente all'interno del modulo. Anche se questo sembra un semplice modulo HTML, abbiamo già definito un modulo supportato da Angular 4 (forse non lo vedi ancora). Quando il FormsModule viene importato, Angular 4 rileva automaticamente un elemento HTML del form e collega il componente NgForm a quell'elemento (tramite il selector del componente NgForm). Questo è il caso del nostro esempio. Sebbene questo modulo Angular 4 sia dichiarato, a questo punto non conosce alcun input supportato da Angular 4. Angular 4 non è così invasivo per registrare ogni elemento HTML di input nel predecessore del form più vicino.
La chiave che consente a un elemento di input di essere notato come elemento Angular 4 e registrato nel componente NgForm è la direttiva NgModel. Quindi, possiamo estendere il modello app.component.tpl.html come segue:
<form> .. <input type="text" name="name" ngModel> .. <input type="text" name="birthYear" ngModel > .. <input type="text" name="country" ngModel/> .. <input type="text" name="city" ngModel/> .. <input type="text" name="phoneNumber[1]" ngModel/> </form>Aggiungendo la direttiva NgModel, tutti gli input vengono registrati nel componente NgForm. Con questo, abbiamo definito un modulo Angular 4 completamente funzionante e finora tutto bene, ma non abbiamo ancora un modo per accedere al componente NgForm e alle funzionalità che offre. Le due principali funzionalità offerte da NgForm sono:
Recupero dei valori di tutti i controlli di input registrati
Recupero dello stato generale di tutti i controlli
Per esporre il NgForm, possiamo aggiungere quanto segue all'elemento <form>:
<form #myForm="ngForm"> .. </form> Ciò è possibile grazie alla proprietà exportAs del decoratore di Component .
Fatto ciò, possiamo accedere ai valori di tutti i controlli di input ed estendere il modello a:
<form #myForm="ngForm"> .. <pre>{{myForm.value | json}}</pre> </form> Con myForm.value ai dati JSON contenenti i valori di tutti gli input registrati e con {{myForm.value | json}} {{myForm.value | json}} , stiamo stampando il JSON con i valori.
Cosa succede se vogliamo avere un sottogruppo di input da un contesto specifico racchiuso in un contenitore e un oggetto separato nei valori JSON, ad esempio, posizione contenente paese e città o numeri di telefono? Non stressarti: anche i moduli basati su modelli in Angular 4 hanno questo. Il modo per raggiungere questo obiettivo è utilizzare la direttiva ngModelGroup .
<form #myForm="ngForm"> .. <div ngModelGroup="location"> .. </div> </div ngModelGroup="phoneNumbers"> .. <div> .. </form>Quello che ci manca ora è un modo per aggiungere più numeri di telefono. Il modo migliore per farlo sarebbe stato utilizzare un array, come la migliore rappresentazione di un contenitore iterabile di più oggetti, ma al momento della stesura di questo articolo, quella funzionalità non è implementata per i moduli basati su modelli. Quindi, dobbiamo applicare una soluzione alternativa per farlo funzionare. La sezione dei numeri di telefono deve essere aggiornata come segue:
<div ngModelGroup="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneId of phoneNumberIds; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" #phoneNumber="ngModel" ngModel/> <button type="button" (click)="remove(i); myForm.control.markAsTouched()">remove</button> </div> <button type="button" (click)="add(); myForm.control.markAsTouched()">Add phone number</button> </div> myForm.control.markAsTouched() viene utilizzato per far touched il modulo in modo da poter visualizzare gli errori in quel momento. I pulsanti non attivano questa proprietà quando vengono cliccati, solo gli input. Per rendere più chiari i prossimi esempi, non aggiungerò questa riga al gestore dei clic per add() e remove() . Immagina che sia lì. (È presente nei Plunkers.)
È inoltre necessario aggiornare AppComponent in modo che contenga il codice seguente:
private count:number = 1; phoneNumberIds:number[] = [1]; remove(i:number) { this.phoneNumberIds.splice(i, 1); } add() { this.phoneNumberIds.push(++this.count); } Dobbiamo memorizzare un ID univoco per ogni nuovo numero di telefono aggiunto e in *ngFor , tracciare i controlli del numero di telefono in base al loro ID (ammetto che non è molto carino, ma fino a quando il team di Angular 4 non implementa questa funzione, temo , è il meglio che possiamo fare)
Ok, cosa abbiamo finora, abbiamo aggiunto il modulo supportato da Angular 4 con input, aggiunto un raggruppamento specifico degli input (posizione e numeri di telefono) ed esposto il modulo all'interno del modello. Ma cosa succede se vorremmo accedere all'oggetto NgForm in qualche metodo nel componente? Daremo un'occhiata a due modi per farlo.
Per il primo modo, NgForm, etichettato myForm nell'esempio corrente, può essere passato come argomento alla funzione che fungerà da gestore per l'evento onSubmit del form. Per una migliore integrazione, l'evento onSubmit è wrapped da un Angular 4, evento specifico di NgForm denominato ngSubmit , e questa è la strada giusta da percorrere se vogliamo eseguire qualche azione all'invio. Quindi, l'esempio ora sarà simile a questo:
<form #myForm="ngForm" (ngSubmit)="register(myForm)"> … </form> Dobbiamo avere un corrispondente metodo register , implementato in AppComponent. Qualcosa di simile a:
register (myForm: NgForm) { console.log('Successful registration'); console.log(myForm); }In questo modo, sfruttando l'evento onSubmit, abbiamo accesso al componente NgForm solo quando viene eseguito submit.
Il secondo modo consiste nell'utilizzare una query di visualizzazione aggiungendo il decoratore @ViewChild a una proprietà del componente.
@ViewChild('myForm') private myForm: NgForm;Con questo approccio, ci è consentito l'accesso al modulo indipendentemente dal fatto che l'evento onSubmit sia stato attivato o meno.
Grande! Ora abbiamo un modulo Angular 4 completamente funzionante con accesso al modulo nel componente. Ma noti che manca qualcosa? Cosa succede se l'utente inserisce qualcosa come "questo non è un anno" nell'input "anni"? Sì, hai capito, ci manca la convalida degli input e lo tratteremo nella sezione seguente.
Convalida
La convalida è davvero importante per ogni applicazione. Vogliamo sempre convalidare l'input dell'utente (non possiamo fidarci dell'utente) per impedire l'invio/il salvataggio di dati non validi e dobbiamo mostrare un messaggio significativo sull'errore per guidare correttamente l'utente a inserire dati validi.
Affinché alcune regole di convalida vengano applicate su alcuni input, il validatore appropriato deve essere associato a quell'input. Angular 4 offre già una serie di validatori comuni come: required , maxLength , minLength …
Quindi, come possiamo associare un validatore a un input? Bene, abbastanza facile; basta aggiungere la direttiva validator al controllo:
<input name="name" ngModel required/>Questo esempio rende obbligatorio l'inserimento del "nome". Aggiungiamo alcune convalide a tutti gli input nel nostro esempio.
<form #myForm="ngForm" (ngSubmit)="actionOnSubmit(myForm)" novalidate> <p>Is "myForm" valid? {{myForm.valid}}</p> .. <input type="text" name="name" ngModel required/> .. <input type="text" name="birthYear" ngModel required pattern="\\d{4,4}"/> .. <div ngModelGroup="location"> .. <input type="text" name="country" ngModel required/> .. <input type="text" name="city" ngModel/> </div> <div ngModelGroup="phoneNumbers"> .. <input type="text" name="phoneNumber[{{phoneId}}]" ngModel required/> .. </div> .. </form>Nota:
novalidateviene utilizzato per disabilitare la convalida del modulo nativo del browser.
Abbiamo reso obbligatorio il “nome”, il campo “anni” è obbligatorio e deve essere composto solo da numeri, è richiesto l'inserimento del Paese ed è richiesto anche il numero di telefono. Inoltre, stampiamo lo stato di validità del modulo con {{myForm.valid}} .
Un miglioramento a questo esempio sarebbe mostrare anche cosa c'è di sbagliato nell'input dell'utente (non solo mostrare lo stato generale). Prima di continuare ad aggiungere ulteriore convalida, vorrei implementare un componente di supporto che ci consentirà di stampare tutti gli errori per un controllo fornito.
// show-errors.component.ts import { Component, Input } from '@angular/core'; import { AbstractControlDirective, AbstractControl } from '@angular/forms'; @Component({ selector: 'show-errors', template: ` <ul *ngIf="shouldShowErrors()"> <li *ngFor="let error of listOfErrors()">{{error}}</li> </ul> `, }) export class ShowErrorsComponent { private static readonly errorMessages = { 'required': () => 'This field is required', 'minlength': (params) => 'The min number of characters is ' + params.requiredLength, 'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength, 'pattern': (params) => 'The required pattern is: ' + params.requiredPattern, 'years': (params) => params.message, 'countryCity': (params) => params.message, 'uniqueName': (params) => params.message, 'telephoneNumbers': (params) => params.message, 'telephoneNumber': (params) => params.message }; @Input() private control: AbstractControlDirective | AbstractControl; shouldShowErrors(): boolean { return this.control && this.control.errors && (this.control.dirty || this.control.touched); } listOfErrors(): string[] { return Object.keys(this.control.errors) .map(field => this.getMessage(field, this.control.errors[field])); } private getMessage(type: string, params: any) { return ShowErrorsComponent.errorMessages[type](params); } }L'elenco degli errori viene visualizzato solo se sono presenti degli errori e l'input è toccato o sporco.
Il messaggio per ogni errore viene cercato in una mappa di messaggi predefiniti errorMessages (ho aggiunto tutti i messaggi in anticipo).
Questo componente può essere utilizzato come segue:
<div> <label>Birth Year</label> <input type="text" name="birthYear" #birthYear="ngModel" ngModel required pattern="\\d{4,4}"/> <show-errors [control]="birthYear"></show-errors> </div>Dobbiamo esporre NgModel per ogni input e passarlo al componente che esegue il rendering di tutti gli errori. Puoi notare che in questo esempio abbiamo utilizzato uno schema per verificare se i dati sono un numero; cosa succede se l'utente inserisce "0000"? Questo sarebbe un input non valido. Inoltre, mancano i validatori per un nome univoco, la strana restrizione del paese (se paese='Francia', la città deve essere 'Parigi'), lo schema per un numero di telefono corretto e la convalida che almeno un numero di telefono esiste. Questo è il momento giusto per dare un'occhiata ai validatori personalizzati.
Angular 4 offre un'interfaccia che ogni validatore personalizzato deve implementare, l'interfaccia Validator (che sorpresa!). L'interfaccia del Validator è sostanzialmente simile a questa:
export interface Validator { validate(c: AbstractControl): ValidationErrors | null; registerOnValidatorChange?(fn: () => void): void; } Dove ogni implementazione concreta DEVE implementare il metodo 'validate'. Questo metodo di validate è davvero interessante su ciò che può essere ricevuto come input e su ciò che dovrebbe essere restituito come output. L'input è un AbstractControl, il che significa che l'argomento può essere qualsiasi tipo che estende AbstractControl (FormGroup, FormControl e FormArray). L'output del metodo di validate deve essere null o undefined (nessun output) se l'input dell'utente è valido oppure restituire un oggetto ValidationErrors se l'input dell'utente non è valido. Con questa conoscenza, ora implementeremo un validatore birthYear personalizzato.
import { Directive } from '@angular/core'; import { NG_VALIDATORS, FormControl, Validator, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[birthYear]', providers: [{provide: NG_VALIDATORS, useExisting: BirthYearValidatorDirective, multi: true}] }) export class BirthYearValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } } Ci sono alcune cose da spiegare qui. Per prima cosa potresti notare che abbiamo implementato l'interfaccia Validator. Il metodo di validate controlla se l'utente ha un'età compresa tra 18 e 85 anni entro l'anno di nascita inserito. Se l'input è valido, viene restituito null oppure viene restituito un oggetto contenente il messaggio di convalida. E l'ultima e più importante parte è dichiarare questa direttiva come Validatore. Ciò viene fatto nel parametro "provider" del decoratore @Directive. Questo validatore viene fornito come un valore del multiprovider NG_VALIDATORS. Inoltre, non dimenticare di dichiarare questa direttiva in NgModule. E ora possiamo usare questo validatore come segue:
<input type="text" name="birthYear" #year="ngModel" ngModel required birthYear/>Sì, così semplice!
Per il numero di telefono, possiamo convalidare il formato del numero di telefono in questo modo:
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[telephoneNumber]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumberFormatValidatorDirective, multi: true}] }) export class TelephoneNumberFormatValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } } Ora arrivano le due convalide, per il paese e il numero di numeri di telefono. Noti qualcosa di comune per entrambi? Entrambi richiedono più di un controllo per eseguire una validazione corretta. Bene, ricordi l'interfaccia del Validator e cosa ne abbiamo detto? L'argomento del metodo validate è AbstractControl, che può essere un input dell'utente o il form stesso. Ciò crea l'opportunità di implementare un validatore che utilizza più controlli per determinare lo stato di convalida concreto.
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[countryCity]', providers: [{provide: NG_VALIDATORS, useExisting: CountryCityValidatorDirective, multi: true}] }) export class CountryCityValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } }Abbiamo implementato un nuovo validatore, validatore paese-città. Puoi notare che ora come argomento il metodo validate riceve un FormGroup e da quel FormGroup possiamo recuperare gli input richiesti per la convalida. Il resto delle cose è molto simile al validatore di input singolo.
Il validatore per il numero di numeri di telefono sarà simile a questo:
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors, FormControl } from '@angular/forms'; @Directive({ selector: '[telephoneNumbers]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumbersValidatorDirective, multi: true}] }) export class TelephoneNumbersValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormGroup> form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }Possiamo usarli in questo modo:
<form #myForm="ngForm" countryCity telephoneNumbers> .. </form>Come i validatori di input, giusto? Solo ora applicato al modulo.
Ricordi il componente ShowErrors? L'abbiamo implementato per funzionare con una AbstractControlDirective, il che significa che potremmo riutilizzarlo per mostrare anche tutti gli errori associati direttamente a questo modulo. Tieni presente che a questo punto le uniche regole di validazione direttamente associate al modulo sono i numeri Country-city e Telephone numbers (gli altri validatori sono associati ai controlli specifici del modulo). Per stampare tutti gli errori di modulo, procedi come segue:
<form #myForm="ngForm" countryCity telephoneNumbers > <show-errors [control]="myForm"></show-errors> .. </form> L'ultima cosa rimasta è la convalida per un nome univoco. Questo è un po' diverso; per verificare se il nome è univoco, molto probabilmente è necessaria una chiamata al back-end per verificare tutti i nomi esistenti. Questo viene classificato come un'operazione asincrona. A questo scopo, possiamo riutilizzare la tecnica precedente per i validatori personalizzati, basta fare in modo che il validate restituisca un oggetto che verrà risolto in futuro (promise o osservabile). Nel nostro caso, useremo una promessa:
import { Directive } from '@angular/core'; import { NG_ASYNC_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[uniqueName]', providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: UniqueNameValidatorDirective, multi: true}] }) export class UniqueNameValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } } Stiamo aspettando 1 secondo e poi restituiamo un risultato. Simile ai validatori di sincronizzazione, se la promessa viene risolta con null , significa che la convalida è andata a buon fine; se la promessa viene risolta con qualcos'altro, la convalida non è riuscita. Si noti inoltre che ora questo validatore è registrato su un altro multi-provider, NG_ASYNC_VALIDATORS . Una proprietà utile dei moduli per quanto riguarda i validatori asincroni è la proprietà pending . Può essere utilizzato in questo modo:
<button [disabled]="myForm.pending">Register</button>Disabiliterà il pulsante fino a quando i validatori asincroni non saranno risolti.
Ecco un Plunker contenente l'AppComponent completo, il componente ShowErrors e tutti i Validator.
Con questi esempi, abbiamo trattato la maggior parte dei casi per l'utilizzo di moduli basati su modelli. Abbiamo dimostrato che i moduli basati su modelli sono molto simili ai moduli in AngularJS (sarà davvero facile per gli sviluppatori AngularJS migrare). Con questo tipo di modulo, è abbastanza facile integrare i moduli Angular 4 con una programmazione minima, principalmente con manipolazioni nel modello HTML.
Forme reattive
Le forme reattive erano anche conosciute come forme "model-driven", ma mi piace chiamarle forme "programmatiche", e presto capirete perché. I moduli reattivi sono un nuovo approccio al supporto dei moduli Angular 4, quindi a differenza dei modelli basati su modelli, gli sviluppatori AngularJS non avranno familiarità con questo tipo.
Possiamo iniziare ora, ricordi come i moduli basati su modelli avevano un modulo speciale? Ebbene, i moduli reattivi hanno anche un proprio modulo, chiamato ReactiveFormsModule e devono essere importati per attivare questo tipo di moduli.
import {ReactiveFormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}Inoltre, non dimenticare di avviare l'applicazione.
Possiamo iniziare con lo stesso AppComponent e lo stesso modello della sezione precedente.
A questo punto, se FormsModule non è importato (e assicurati che non lo sia), abbiamo solo un normale elemento del modulo HTML con un paio di controlli del modulo, nessuna magia angolare qui.
Arriviamo al punto in cui noterai perché mi piace chiamare questo approccio "programmatico". Per abilitare i moduli Angular 4, dobbiamo dichiarare manualmente l'oggetto FormGroup e popolarlo con controlli come questo:

import { FormGroup, FormControl, FormArray, NgForm } from '@angular/forms'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'my-app', templateUrl: 'src/app.component.html' }) export class AppComponent implements OnInit { private myForm: FormGroup; constructor() { } ngOnInit() { this.myForm = new FormGroup({ 'name': new FormControl(), 'birthYear': new FormControl(), 'location': new FormGroup({ 'country': new FormControl(), 'city': new FormControl() }), 'phoneNumbers': new FormArray([new FormControl('')]) }); } printMyForm() { console.log(this.myForm); } register(myForm: NgForm) { console.log('Registration successful.'); console.log(myForm.value); } } I metodi printForm e register sono gli stessi degli esempi precedenti e verranno utilizzati nei passaggi successivi. I tipi di chiave qui utilizzati sono FormGroup, FormControl e FormArray. Questi tre tipi sono tutto ciò di cui abbiamo bisogno per creare un FormGroup valido. Il FormGroup è facile; è un semplice contenitore di controlli. Anche FormControl è facile; è qualsiasi controllo (ad esempio, input). Infine, FormArray è il pezzo del puzzle che ci mancava nell'approccio basato sui modelli. Il FormArray consente di mantenere un gruppo di controlli senza specificare una chiave concreta per ogni controllo, fondamentalmente una serie di controlli (sembra la cosa perfetta per i numeri di telefono, giusto?).
Quando costruisci uno di questi tre tipi, ricorda questa regola dei 3. Il costruttore per ogni tipo riceve tre argomenti: value , validatore o elenco di validatori e validatore asincrono o elenco di validatori asincroni, definiti nel codice:
constructor(value: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]); Per FormGroup, il value è un oggetto in cui ogni chiave rappresenta il nome di un controllo e il valore è il controllo stesso.
Per FormArray, il value è una matrice di controlli.
Per FormControl, il value è il valore iniziale o lo stato iniziale (oggetto contenente un value e una proprietà disabled ) del controllo.
Abbiamo creato l'oggetto FormGroup, ma il modello non è ancora a conoscenza di questo oggetto. Il collegamento tra FormGroup nel componente e il modello viene eseguito con quattro direttive: formGroup , formControlName , formGroupName e formArrayName , usate in questo modo:
<form [formGroup]="myForm" (ngSubmit)="register(myForm)"> <div> <label>Name</label> <input type="text" name="name" formControlName="name"> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear" formControlName="birthYear"> </div> <div formGroupName="location"> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country" formControlName="country"> </div> <div> <label>City</label> <input type="text" name="city" formControlName="city"> </div> </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneNumberControl of myForm.controls.phoneNumbers.controls; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i"> <button type="button" (click)="remove(i)">remove</button> </div> <button type="button" (click)="add()">Add phone number</button> </div> <pre>{{myForm.value | json}}</pre> <button type="submit">Register</button> <button type="button" (click)="printMyForm()">Print to console</button> </form>Ora che abbiamo FormArray, puoi vedere che possiamo usare quella struttura per il rendering di tutti i numeri di telefono.
E ora per aggiungere il supporto per l'aggiunta e la rimozione di numeri di telefono (nel componente):
remove(i: number) { (<FormArray>this.myForm.get('phoneNumbers')).removeAt(i); } add() { (<FormArray>this.myForm.get('phoneNumbers')).push(new FormControl('')); }Ora abbiamo una forma reattiva Angular 4 completamente funzionante. Notare la differenza rispetto ai moduli basati su modelli in cui il FormGroup è stato "creato nel modello" (scansionando la struttura del modello) e passato al componente, nei moduli reattivi è il contrario, il FormGroup completo viene creato nel componente, quindi “passato al template” e collegato con i controlli corrispondenti. Ma, ancora una volta, abbiamo lo stesso problema con la convalida, un problema che verrà risolto nella prossima sezione.
Convalida
Quando si tratta di convalida, i moduli reattivi sono molto più flessibili rispetto ai moduli basati su modelli. Senza ulteriori modifiche, possiamo riutilizzare gli stessi validatori che sono stati implementati in precedenza (per il modello basato). Quindi, aggiungendo le direttive del validatore, possiamo attivare la stessa validazione:
<form [formGroup]="myForm" (ngSubmit)="register(myForm)" countryCity telephoneNumbers novalidate> <input type="text" name="name" formControlName="name" required uniqueName> <show-errors [control]="myForm.controls.name"></show-errors> .. <input type="text" name="birthYear" formControlName="birthYear" required birthYear> <show-errors [control]="myForm.controls.birthYear"></show-errors> .. <div formGroupName="location"> .. <input type="text" name="country" formControlName="country" required> <show-errors [control]="myForm.controls.location.controls.country"></show-errors> .. <input type="text" name="city" formControlName="city"> .. </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> .. <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i" required telephoneNumber> <show-errors [control]="phoneNumberControl"></show-errors> .. </div> .. </form>Tieni presente che ora non abbiamo la direttiva NgModel da passare al componente ShowErrors, ma il FormGroup completo è già costruito e possiamo passare il corretto AbstractControl per recuperare gli errori.
Ecco un Plunker completamente funzionante con questo tipo di convalida per le forme reattive.
Ma non sarebbe divertente se riutilizzassimo solo i validatori, giusto? Vedremo come specificare i validatori durante la creazione del gruppo di moduli.
Ricordi la regola della "regola 3s" che abbiamo menzionato sul costruttore per FormGroup, FormControl e FormArray? Sì, abbiamo detto che il costruttore può ricevere funzioni di validazione. Quindi, proviamo questo approccio.
Innanzitutto, dobbiamo estrarre le funzioni di validate di tutti i validatori in una classe esponendole come metodi statici:
import { FormArray, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; export class CustomValidators { static birthYear(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } static countryCity(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } static uniqueName(c: FormControl): Promise<ValidationErrors> { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } static telephoneNumber(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } static telephoneNumbers(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormArray>form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }Now we can change the creation of 'myForm' to:
this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()]) }, Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) );Vedere? The rule of “3s,” when defining a FormControl, multiple validators can be declared in an array, and if we want to add multiple validators to a FormGroup they must be “merged” using Validators.compose (also Validators.composeAsync is available). And, that's it, validation should be working completely. There's a Plunker for this example as well.
This goes out to everybody that hates the “new” word. For working with the reactive forms, there's a shortcut provided—a builder, to be more precise. The FormBuilder allows creating the complete FormGroup by using the “builder pattern.” And that can be done by changing the FormGroup construction like this:
constructor(private fb: FormBuilder) { } ngOnInit() { this.myForm = this.fb.group({ 'name': ['', Validators.required, CustomValidators.uniqueName], 'birthYear': ['', [Validators.required, CustomValidators.birthYear]], 'location': this.fb.group({ 'country': ['', Validators.required], 'city': '' }), 'phoneNumbers': this.fb.array([this.buildPhoneNumberComponent()]) }, { validator: Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) } ); }Not a very big improvement from the instantiation with “new,” but there it is. And, don't worry, there's a Plunker for this also.
In this second section, we had a look at reactive forms in Angular 4. As you may notice, it is a completely new approach towards adding support for forms. Even though it seems verbose, this approach gives the developer total control over the underlying structure that enables forms in Angular 4. Also, since the reactive forms are created manually in the component, they are exposed and provide an easy way to be tested and controlled, while this was not the case with the template-driven forms.
Nesting Forms
Nesting forms is in some cases useful and a required feature, mainly when the state (eg, validity) of a sub-group of controls needs to determined. Think about a tree of components; we might be interested in the validity of a certain component in the middle of that hierarchy. That would be really hard to achieve if we had a single form at the root component. But, oh boy, it is a sensitive manner on a couple of levels. First, nesting real HTML forms, according to the HTML specification, is not allowed. We might try to nest <form> elements. In some browsers it might actually work, but we cannot be sure that it will work on all browsers, since it is not in the HTML spec. In AngularJS, the way to work around this limitation was to use the ngForm directive, which offered the AngularJS form functionalities (just grouping of the controls, not all form capabilities like posting to the server) but could be placed on any element. Also, in AngularJS, nesting of forms (when I say forms, I mean NgForm) was available out of the box. Just by declaring a tree of couple of elements with the ngForm directive, the state of each form was propagated upwards to the root element.
In the next section, we will have a look at a couple options on how to nest forms. I like to point out that we can differentiate two types of nesting: within the same component and across different components.
Nesting within the Same Component
If you take a look at the example that we implemented with the template-driven and the reactive approach, you will notice that we have two inner containers of controls, the “location” and the “phone numbers.” To create that container, to store the values in a separate property object, we used the NgModelGroup, FormGroupName, and the FormArrayName directives. If you have a good look at the definition of each directive, you may notice that each one of them extends the ControlContainer class (directly or indirectly). Well, what do you know, it turns out this is enough to provide the functionality that we require, wrapping up the state of all inner controls and propagating that state to the parent.
For the template-driven form, we need to do the following changes:
<form #myForm="ngForm" (ngSubmit)="register(myForm)" novalidate> .. <div ngModelGroup="location" #location="ngModelGroup" countryCity> .. <show-errors [control]="location"></show-errors> </div> <div ngModelGroup="phoneNumbers" #phoneNumbers="ngModelGroup" telephoneNumbers> .. <show-errors [control]="phoneNumbers"></show-errors> </div> </form> We added the ShowErrors component to each group, to show the errors directly associated with that group only. Since we moved the countryCity and telephoneNumbers validators to a different level, we also need to update them appropriately:
// country-city-validator.directive.ts let countryControl = form.get('country'); let cityControl = form.get('city');And telephone-numbers-validator.directive.ts to:
let phoneNumbers = form.controls; let hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers).length > 0;You can try the full example with template-driven forms in this Plunker.
And for the reactive forms, we will need some similar changes:
<form [formGroup]="myForm" (ngSubmit)="register(myForm)" novalidate> .. <div formGroupName="location"> .. <show-errors [control]="myForm.controls.location"></show-errors> </div> <div formArrayName="phoneNumbers"> .. <show-errors [control]="myForm.controls.phoneNumbers"></show-errors> </div> .. </form> The same changes from country-city-validator.directive.ts and telephone-numbers-validator.directive.ts are required for the countryCity and telephoneNumbers validators in CustomValidators to properly locate the controls.
And lastly, we need to modify the construction of the FormGroup to:
this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }, CustomValidators.countryCity), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()], CustomValidators.telephoneNumbers) });And there you have it—we've improved the validation for the reactive forms as well and as expected, the Plunker for this example.
Nesting across Different Components
It may come as a shock to all AngularJS developers, but in Angular 4, nesting of forms across different component doesn't work out of the box. I'm going to be straight honest with you; my opinion is that nesting is not supported for a reason (probably not because the Angular 4 team just forgot about it). Angular4's main enforced principle is a one-way data flow, top to bottom through the tree of components. The whole framework was designed like that, where the vital operation, the change detection, is executed in the same manner, top to bottom. If we follow this principle completely, we should have no issues, and all changes should be resolved within one full detection cycle. That's the idea, at least. In order to check that one-way data flow is implemented correctly, the nice guys in the Angular 4 team implemented a feature that after each change detection cycle, while in development mode, an additional round of change detection is triggered to check that no binding was changed as a result of reverse data propagation. What this means, let's think about a tree of components (C1, C2, C3, C4) as in Fig. 1, the change detection starts at the C1 component, continues at the C2 component and ends in the C3 component.
If we have some method in C3 with a side effect that changes some binding in C1, that means that we are pushing data upwards, but the change detection for C1 already passed. When working in dev mode, the second round kicks in and notices a change in C1 that came as a result of a method execution in some child component. Then you are in trouble and you'll probably see the “Expression has changed after it was checked” exception. You could just turn off the development mode and there will be no exception, but the problem will not be solved; plus, how would you sleep at night, just sweeping all your problems under the rug like that?
Una volta che lo sai, pensa a cosa stiamo facendo se aggreghiamo lo stato dei moduli. Esatto, i dati vengono spinti verso l'alto nell'albero dei componenti. Anche quando si lavora con moduli a livello singolo, l'integrazione dei controlli del modulo ( ngModel ) e del modulo stesso non è così piacevole. Attivano un ciclo di rilevamento delle modifiche aggiuntivo durante la registrazione o l'aggiornamento del valore di un controllo (viene fatto utilizzando una promessa risolta, ma la mantengono segreta). Perché è necessario un round aggiuntivo? Ebbene, per lo stesso motivo, i dati fluiscono verso l'alto, dal controllo al modulo. Ma, forse, a volte, l'annidamento di moduli su più componenti è una funzionalità richiesta e dobbiamo pensare a una soluzione per supportare questo requisito.
Con quello che sappiamo finora, la prima idea che viene in mente è usare i moduli reattivi, creare l'albero dei moduli completo in qualche componente radice e quindi passare i moduli figlio ai componenti figlio come input. In questo modo hai accoppiato strettamente il genitore con i componenti figlio e hai ingombrato la logica aziendale del componente radice con la gestione della creazione di tutti i moduli figlio. Dai, siamo professionisti, sono sicuro che possiamo trovare un modo per creare componenti totalmente isolati con moduli e fornire un modo in cui il modulo si limiti a propagare lo stato a chiunque sia il genitore.
Detto questo, ecco una direttiva che consente di annidare i moduli Angular 4 (implementato perché necessario per un progetto):
import { OnInit, OnDestroy, Directive, SkipSelf, Optional, Attribute, Injector, Input } from '@angular/core'; import { NgForm, FormArray, FormGroup, AbstractControl } from '@angular/forms'; const resolvedPromise = Promise.resolve(null); @Directive({ selector: '[nestableForm]' }) export class NestableFormDirective implements OnInit, OnDestroy { private static readonly FORM_ARRAY_NAME = 'CHILD_FORMS'; private currentForm: FormGroup; @Input() private formGroup: FormGroup; constructor(@SkipSelf() @Optional() private parentForm: NestableFormDirective, private injector: Injector, @Attribute('rootNestableForm') private isRoot) { } ngOnInit() { if (!this.currentForm) { // NOTE: at this point both NgForm and ReactiveFrom should be available this.executePostponed(() => this.resolveAndRegister()); } } ngOnDestroy() { this.executePostponed(() => this.parentForm.removeControl(this.currentForm)); } public registerNestedForm(control: AbstractControl): void { // NOTE: prevent circular reference (adding to itself) if (control === this.currentForm) { throw new Error('Trying to add itself! Nestable form can be added only on parent "NgForm" or "FormGroup".'); } (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)).push(control); } public removeControl(control: AbstractControl): void { const array = (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)); const idx = array.controls.indexOf(control); array.removeAt(idx); } private resolveAndRegister(): void { this.currentForm = this.resolveCurrentForm(); this.currentForm.addControl(NestableFormDirective.FORM_ARRAY_NAME, new FormArray([])); this.registerToParent(); } private resolveCurrentForm(): FormGroup { // NOTE: template-driven or model-driven => determined by the formGroup input return this.formGroup ? this.formGroup : this.injector.get(NgForm).control; } private registerToParent(): void { if (this.parentForm != null && !this.isRoot) { this.parentForm.registerNestedForm(this.currentForm); } } private executePostponed(callback: () => void): void { resolvedPromise.then(() => callback()); } } L'esempio nella seguente GIF mostra un componente main contenente form-1 e, all'interno di quel form, c'è un altro componente nidificato, component-2 . component-2 contiene form-2 , che ha nidificato form-2.1 , form-2.2 e un componente ( component-3 ) che contiene un albero di una forma reattiva e un componente ( component-4 ) che contiene una forma che è isolato da tutte le altre forme. Abbastanza disordinato, lo so, ma volevo creare uno scenario piuttosto complesso per mostrare la funzionalità di questa direttiva.
L'esempio è implementato in questo Plunker.
Le caratteristiche che offre sono:
Abilita la nidificazione aggiungendo la direttiva nestableForm agli elementi: form, ngForm, [ngForm], [formGroup]
Funziona con moduli basati su modelli e reattivi
Consente di creare un albero di moduli che si estende su più componenti
Isola un sottoalbero di moduli con rootNestableForm="true" (non si registrerà nel genitore nestableForm)
Questa direttiva consente a un modulo in un componente figlio di registrarsi nel primo genitore nestableForm, indipendentemente dal fatto che il modulo padre sia dichiarato nello stesso componente o meno. Entreremo nei dettagli dell'implementazione.
Prima di tutto, diamo un'occhiata al costruttore. Il primo argomento è:
@SkipSelf() @Optional() private parentForm: NestableFormDirectiveQuesto cerca il primo genitore NestableFormDirective. @SkipSelf, per non corrispondere a se stesso, e @Optional perché potrebbe non trovare un genitore, nel caso del modulo radice. Ora abbiamo un riferimento al modulo annidabile genitore.
Il secondo argomento è:
private injector: Injector L'iniettore viene utilizzato per recuperare il provider FormGroup corrente (modello o reattivo).
E l'ultimo argomento è:
@Attribute('rootNestableForm') private isRootper ottenere il valore che determina se questo modulo è isolato dall'albero dei moduli.
Successivamente, su ngInit come azione posticipata (ricordate il flusso di dati inverso?), il FormGroup corrente viene risolto, un nuovo controllo FormArray denominato CHILD_FORMS viene registrato in questo FormGroup (dove verranno registrati i moduli figlio) e come ultima azione, il FormGroup corrente è registrato come figlio nel modulo annidabile padre.
L'azione ngOnDestroy viene eseguita quando il modulo viene distrutto. Alla distruzione, sempre come azione posticipata, il modulo attuale viene rimosso dal genitore (cancellazione).
La direttiva per i moduli annidabili può essere ulteriormente personalizzata per un'esigenza specifica, ad esempio rimuovere il supporto per i moduli reattivi, registrare ogni modulo figlio con un nome specifico (non in un array CHILD_FORMS) e così via. Questa implementazione della direttiva nestableForm ha soddisfatto i requisiti del progetto ed è qui presentata come tale. Copre alcuni casi di base come l'aggiunta di un nuovo modulo o la rimozione dinamica di un modulo esistente (*ngIf) e la propagazione dello stato del modulo al genitore. Questo sostanzialmente si riduce a operazioni che possono essere risolte entro un ciclo di rilevamento delle modifiche (con rinvio o meno).
Se desideri uno scenario più avanzato come l'aggiunta di una convalida condizionale a un input (ad es. [required]="qualchecondizione") che richiederebbe 2 round di rilevamento delle modifiche, non funzionerà a causa della regola "risoluzione del ciclo di rilevamento" imposto da Angular 4.
Ad ogni modo, se prevedi di utilizzare questa direttiva o di implementare qualche altra soluzione, fai molta attenzione alle cose che sono state menzionate relative al rilevamento delle modifiche. A questo punto, ecco come viene implementato Angular 4. Potrebbe cambiare in futuro, non possiamo saperlo. L'attuale configurazione e la restrizione imposta in Angular 4 menzionate in questo articolo potrebbero essere uno svantaggio o un vantaggio. Resta da vedere.
Forme semplificate con Angular 4
Come puoi vedere, il team di Angular ha svolto un ottimo lavoro nel fornire molte funzionalità relative ai moduli. Spero che questo post serva da guida completa per lavorare con i diversi tipi di moduli in Angular 4, fornendo anche informazioni su alcuni concetti più avanzati come l'annidamento dei moduli e il processo di rilevamento delle modifiche.
Nonostante tutti i diversi post relativi ai moduli Angular 4 (o qualsiasi altro argomento di Angular 4 per quella materia), secondo me, il miglior punto di partenza è la documentazione ufficiale di Angular 4. Inoltre, i ragazzi di Angular hanno una buona documentazione nel loro codice. Molte volte, ho trovato una soluzione semplicemente guardando il loro codice sorgente e la documentazione lì, senza Google o altro. Riguardo all'annidamento dei moduli, discusso nell'ultima sezione, credo che qualsiasi sviluppatore AngularJS che inizi ad imparare Angular 4 inciamperà in questo problema ad un certo punto, che è stata una specie di mia ispirazione per scrivere questo post.
Come abbiamo anche visto, ci sono due tipi di moduli e non esiste una regola rigida per cui non puoi usarli insieme. È bello mantenere la base di codice pulita e coerente, ma a volte è possibile fare qualcosa più facilmente con i moduli basati su modelli e, a volte, è il contrario. Quindi, se non ti dispiace le dimensioni del pacchetto leggermente più grandi, ti suggerisco di utilizzare quello che ritieni più appropriato caso per caso. Basta non mescolarli all'interno dello stesso componente perché probabilmente creerà un po' di confusione.
Plunker usati in questo post
Moduli basati su modelli
Moduli reattivi, validatori di modelli
Moduli reattivi, validatori di codici
Forme reattive, generatore di moduli
Moduli basati su modelli, nidificati all'interno dello stesso componente
Forme reattive, annidate all'interno dello stesso componente
Forme nidificate attraverso l'albero dei componenti
