Lucrul cu Angular 4 Forms: Imbricare și validare a intrărilor
Publicat: 2022-03-11Pe web, unele dintre cele mai vechi elemente de introducere a utilizatorului au fost un buton, caseta de selectare, introducerea textului și butoanele radio. Până în ziua de azi, aceste elemente sunt încă folosite în aplicațiile web moderne, deși standardul HTML a parcurs un drum lung de la definirea sa timpurie și permite acum tot felul de interacțiuni fanteziste.
Validarea intrărilor utilizatorilor este o parte esențială a oricărei aplicații web robuste.
Formularele din aplicațiile Angular pot agrega starea tuturor intrărilor care se află sub acea formă și pot oferi o stare generală precum starea de validare a formularului complet. Acest lucru poate fi foarte util pentru a decide dacă intrarea utilizatorului va fi acceptată sau respinsă fără a verifica fiecare intrare separat.
În acest articol, veți afla cum puteți lucra cu formulare și să efectuați validarea formularelor cu ușurință în aplicația dvs. Angular.
În Angular 4, există două tipuri diferite de formulare disponibile pentru a lucra: formulare bazate pe șablon și formulare reactive. Vom parcurge fiecare tip de formular folosind același exemplu pentru a vedea cum aceleași lucruri pot fi implementate în moduri diferite. Mai târziu, în articol, vom analiza o abordare nouă a modului de a configura și de a lucra cu forme imbricate.
Angular 4 Forme
În Angular 4, următoarele patru stări sunt utilizate în mod obișnuit de formulare:
valid – starea validității tuturor controalelor de formular, adevărat dacă toate controalele sunt valide
invalid – inversul lui
valid
; adevărat dacă un anumit control este invalidcurat – dă un statut despre „curățenia” formei; adevărat dacă nu a fost modificat niciun control
murdar – inversul lui
pristine
; adevărat dacă un anumit control a fost modificat
Să aruncăm o privire la un exemplu de bază al unui formular:
<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>
Specificația pentru acest exemplu este următoarea:
nume - este obligatoriu și unic printre toți utilizatorii înregistrați
birthYear - ar trebui să fie un număr valid și utilizatorul trebuie să aibă cel puțin 18 și mai puțin de 85 de ani
tara - este obligatoriu, si doar ca sa complicam putin lucrurile, avem nevoie de o validare ca daca tara este Franta, atunci orasul trebuie sa fie Paris (sa zicem ca serviciul nostru este oferit doar in Paris)
phoneNumber – fiecare număr de telefon trebuie să urmeze un model specificat, trebuie să existe cel puțin un număr de telefon și utilizatorului i se permite să adauge un număr de telefon nou sau să elimine un număr de telefon existent.
Butonul „Înregistrare” este activat numai dacă toate intrările sunt valide și, odată apăsat, trimite formularul.
„Print to Console” doar tipărește valoarea tuturor intrărilor în consolă atunci când se face clic.
Scopul final este implementarea completă a specificației definite.
Formulare bazate pe șabloane
Formele bazate pe șabloane sunt foarte asemănătoare cu formele din AngularJS (sau Angular 1, așa cum se referă unii). Deci, cineva care a lucrat cu formulare în AngularJS va fi foarte familiarizat cu această abordare a lucrului cu formulare.
Odată cu introducerea modulelor în Angular 4, se impune ca fiecare tip specific de formă să fie într-un modul separat și trebuie să definim în mod explicit ce tip vom folosi importând modulul corespunzător. Modulul respectiv pentru formularele bazate pe șablon este FormsModule. Acestea fiind spuse, puteți activa formularele bazate pe șablon după cum urmează:
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 {}
După cum este prezentat în acest fragment de cod, mai întâi trebuie să importam modulul browser, deoarece „oferă servicii care sunt esențiale pentru lansarea și rularea unei aplicații de browser”. (din documentele Angular 4). Apoi importăm FormsModule necesar pentru a activa formularele bazate pe șablon. Și ultima este declararea componentei rădăcină, AppComponent, unde în următorii pași vom implementa formularul.
Rețineți că în acest exemplu și în exemplele următoare, trebuie să vă asigurați că aplicația este bootstrap corect folosind metoda platformBrowserDynamic
.
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {AppModule} from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
Putem presupune că AppComponent (app.component.ts) arată cam așa:
import {Component} from '@angular/core' @Component({ selector: 'my-app', templateUrl: 'src/app.component.tpl.html' }) export class AppComponent { }
Unde șablonul acestei componente este în app.component.tpl.html și putem copia șablonul inițial în acest fișier.
Observați că fiecare element de intrare trebuie să aibă atributul name
pentru a fi identificat corect în formular. Deși pare un simplu formular HTML, am definit deja un formular suportat pentru Angular 4 (poate că nu îl vedeți încă). Când FormsModule este importat, Angular 4 detectează automat un element HTML al form
și atașează componenta NgForm la acel element (prin selector
componentei NgForm). Acesta este cazul în exemplul nostru. Deși această formă Angular 4 este declarată, în acest moment nu cunoaște nicio intrare Angular 4 acceptată. Angular 4 nu este atât de invaziv pentru a înregistra fiecare element HTML de input
la cel mai apropiat strămoș al form
.
Cheia care permite ca un element de intrare să fie observat ca element Angular 4 și înregistrat la componenta NgForm este directiva NgModel. Deci, putem extinde șablonul app.component.tpl.html după cum urmează:
<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>
Prin adăugarea directivei NgModel, toate intrările sunt înregistrate în componenta NgForm. Cu aceasta, am definit o formă Angular 4 complet funcțională și până acum, atât de bună, dar încă nu avem o modalitate de a accesa componenta NgForm și funcționalitățile pe care le oferă. Cele două funcționalități principale oferite de NgForm sunt:
Recuperarea valorilor tuturor controalelor de intrare înregistrate
Preluarea stării generale a tuturor controalelor
Pentru a expune NgForm, putem adăuga următoarele la elementul <form>:
<form #myForm="ngForm"> .. </form>
Acest lucru este posibil datorită proprietății exportAs
a decoratorului Component
.
Odată făcut acest lucru, putem accesa valorile tuturor controalelor de intrare și putem extinde șablonul la:
<form #myForm="ngForm"> .. <pre>{{myForm.value | json}}</pre> </form>
Cu myForm.value
date JSON care conțin valorile tuturor intrărilor înregistrate și cu {{myForm.value | json}}
{{myForm.value | json}}
, imprimăm JSON-ul cu valorile.
Ce se întâmplă dacă dorim să avem un subgrup de intrări dintr-un context specific învelit într-un container și obiect separat în valorile JSON, de exemplu, locația care conține țara și orașul sau numerele de telefon? Nu vă stresați - formularele bazate pe șabloane din Angular 4 au și asta acoperit. Modul de a realiza acest lucru este prin utilizarea directivei ngModelGroup
.
<form #myForm="ngForm"> .. <div ngModelGroup="location"> .. </div> </div ngModelGroup="phoneNumbers"> .. <div> .. </form>
Ceea ce ne lipsește acum este o modalitate de a adăuga mai multe numere de telefon. Cel mai bun mod de a face acest lucru ar fi fost să folosiți o matrice, ca cea mai bună reprezentare a unui container iterabil de mai multe obiecte, dar în momentul scrierii acestui articol, acea caracteristică nu este implementată pentru formularele bazate pe șablon. Deci, trebuie să aplicăm o soluție pentru ca acest lucru să funcționeze. Secțiunea numere de telefon trebuie actualizată după cum urmează:
<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()
este folosit pentru a face forma touched
, astfel încât să putem afișa erorile în acel moment. Butoanele nu activează această proprietate la apăsare, ci doar intrările. Pentru a face următoarele exemple mai clare, nu voi adăuga această linie pe handlerul de clic pentru add()
și remove()
. Doar imaginați-vă că este acolo. (Este prezent în Plunkers.)
De asemenea, trebuie să actualizăm AppComponent
pentru a conține următorul cod:
private count:number = 1; phoneNumberIds:number[] = [1]; remove(i:number) { this.phoneNumberIds.splice(i, 1); } add() { this.phoneNumberIds.push(++this.count); }
Trebuie să stocăm un ID unic pentru fiecare număr de telefon nou adăugat și, în *ngFor
, să urmărim controalele numărului de telefon după id-ul lor (recunosc că nu este foarte frumos, dar până când echipa Angular 4 va implementa această caracteristică, mă tem , este tot ce putem face)
Bine, ce avem până acum, am adăugat formularul suportat Angular 4 cu intrări, am adăugat o grupare specifică a intrărilor (locație și numere de telefon) și am expus formularul în șablon. Dar dacă am dori să accesăm obiectul NgForm într-o metodă din componentă? Vom arunca o privire la două moduri de a face acest lucru.
Pentru prima modalitate, NgForm, etichetat myForm
în exemplul curent, poate fi transmis ca argument funcției care va servi ca handler pentru evenimentul onSubmit al formularului. Pentru o mai bună integrare, evenimentul onSubmit este wrapped
cu un eveniment Angular 4, specific NgForm, numit ngSubmit
și aceasta este calea corectă dacă vrem să executăm o acțiune la trimitere. Deci, exemplul de acum va arăta astfel:
<form #myForm="ngForm" (ngSubmit)="register(myForm)"> … </form>
Trebuie să avem un register
de metodă corespunzător, implementat în AppComponent. Ceva asemănător cu:
register (myForm: NgForm) { console.log('Successful registration'); console.log(myForm); }
În acest fel, prin folosirea evenimentului onSubmit, avem acces la componenta NgForm doar atunci când trimiterea este executată.
A doua modalitate este de a folosi o interogare de vizualizare prin adăugarea decoratorului @ViewChild la o proprietate a componentei.
@ViewChild('myForm') private myForm: NgForm;
Cu această abordare, ni se permite accesul la formular, indiferent dacă evenimentul onSubmit a fost declanșat sau nu.
Grozav! Acum avem un formular Angular 4 complet funcțional, cu acces la formularul din componentă. Dar, observați că lipsește ceva? Ce se întâmplă dacă utilizatorul introduce ceva de genul „acest-nu-este-un-an” în intrarea „ani”? Da, ați înțeles, ne lipsește validarea intrărilor și o vom acoperi în secțiunea următoare.
Validare
Validarea este foarte importantă pentru fiecare aplicație. Dorim întotdeauna să validăm intrarea utilizatorului (nu putem avea încredere în utilizator) pentru a preveni trimiterea/salvarea datelor nevalide și trebuie să arătăm un mesaj semnificativ despre eroare pentru a ghida corect utilizatorul să introducă date valide.
Pentru ca o anumită regulă de validare să fie aplicată pe o anumită intrare, validatorul adecvat trebuie să fie asociat cu acea intrare. Angular 4 oferă deja un set de validatoare comune precum: required
, maxLength
, minLength
...
Deci, cum putem asocia un validator cu o intrare? Ei bine, destul de ușor; doar adăugați directiva validator la control:
<input name="name" ngModel required/>
Acest exemplu face ca introducerea „numelui” să fie obligatorie. Să adăugăm câteva validări la toate intrările din exemplul nostru.
<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>
Notă:
novalidate
este folosit pentru a dezactiva validarea formularului nativ al browserului.
Am făcut obligatoriu „numele”, câmpul „ani” este obligatoriu și trebuie să conțină numai numere, este necesară introducerea țării și, de asemenea, numărul de telefon. De asemenea, imprimăm starea de valabilitate a formularului cu {{myForm.valid}}
.
O îmbunătățire a acestui exemplu ar fi să arăți și ce este în neregulă cu intrarea utilizatorului (nu doar să arăți starea generală). Înainte de a continua să adăugăm o validare suplimentară, aș dori să implementez o componentă de ajutor care ne va permite să tipărim toate erorile pentru un control furnizat.
// 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); } }
Lista cu erori este afișată numai dacă există unele erori și intrarea este atinsă sau murdară.
Mesajul pentru fiecare eroare este căutat într-o hartă a mesajelor predefinite errorMessages
(am adăugat toate mesajele din față).
Această componentă poate fi utilizată după cum urmează:
<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>
Trebuie să expunem NgModel pentru fiecare intrare și să-l transmitem componentei care redă toate erorile. Puteți observa că în acest exemplu am folosit un model pentru a verifica dacă datele sunt un număr; ce se întâmplă dacă utilizatorul introduce „0000”? Aceasta ar fi o intrare nevalidă. De asemenea, ne lipsesc validatorii pentru un nume unic, restricția ciudată a țării (dacă țara='Franța', atunci orașul trebuie să fie 'Paris'), modelul pentru un număr de telefon corect și validarea că cel puțin un număr de telefon există. Acesta este momentul potrivit pentru a arunca o privire la validatorii personalizați.
Angular 4 oferă o interfață pe care fiecare validator personalizat trebuie să o implementeze, interfața Validator (ce surpriză!). Interfața Validator arată, practic, așa:
export interface Validator { validate(c: AbstractControl): ValidationErrors | null; registerOnValidatorChange?(fn: () => void): void; }
Unde fiecare implementare concretă TREBUIE să implementeze metoda „validare”. Această metodă de validate
este cu adevărat interesantă cu privire la ceea ce poate fi primit ca intrare și ce ar trebui returnat ca ieșire. Intrarea este un AbstractControl, ceea ce înseamnă că argumentul poate fi orice tip care extinde AbstractControl (FormGroup, FormControl și FormArray). Ieșirea metodei de validate
ar trebui să fie null
sau undefined
(fără ieșire) dacă intrarea utilizatorului este validă sau să returneze un obiect ValidationErrors
dacă intrarea utilizatorului este nevalidă. Cu aceste cunoștințe, acum vom implementa un validator personalizat birthYear
.
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; } }
Sunt câteva lucruri de explicat aici. Mai întâi puteți observa că am implementat interfața Validator. Metoda de validate
verifică dacă utilizatorul are între 18 și 85 de ani până la anul de naștere introdus. Dacă intrarea este validă, atunci este returnat null
sau, altfel, este returnat un obiect care conține mesajul de validare. Și ultima și cea mai importantă parte este declararea acestei directive ca validator. Acest lucru se face în parametrul „furnizori” al decoratorului @Directive. Acest validator este furnizat ca o valoare a furnizorului multiplu NG_VALIDATORS. De asemenea, nu uitați să declarați această directivă în NgModule. Și acum putem folosi acest validator după cum urmează:
<input type="text" name="birthYear" #year="ngModel" ngModel required birthYear/>
Da, la fel de simplu!
Pentru numărul de telefon, putem valida formatul numărului de telefon astfel:
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; } }
Acum vin cele două validări, pentru țară și numărul de numere de telefon. Observați ceva comun pentru amândoi? Ambele necesită mai mult de un control pentru a efectua o validare adecvată. Ei bine, vă amintiți interfața Validator și ce am spus despre ea? Argumentul metodei de validate
este AbstractControl, care poate fi o intrare de utilizator sau formularul în sine. Acest lucru creează oportunitatea de a implementa un validator care utilizează mai multe controale pentru a determina starea concretă de validare.
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; } } }
Am implementat un nou validator, un validator țară-oraș. Puteți observa că acum ca argument metoda validate primește un FormGroup și din acel FormGroup putem prelua intrările necesare pentru validare. Restul lucrurilor sunt foarte asemănătoare cu validatorul de intrare unică.
Validatorul pentru numărul de numere de telefon va arăta astfel:
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; } }
Le putem folosi astfel:
<form #myForm="ngForm" countryCity telephoneNumbers> .. </form>
La fel ca și validatorii de intrare, nu? Tocmai acum aplicat la formular.
Vă amintiți componenta ShowErrors? L-am implementat pentru a funcționa cu un AbstractControlDirective, ceea ce înseamnă că l-am putea reutiliza pentru a afișa și toate erorile asociate direct cu acest formular. Rețineți că în acest moment singurele reguli de validare asociate direct cu formularul sunt Country-city
și Telephone numbers
(ceilalți validatori sunt asociați cu controalele specifice formularului). Pentru a tipări toate erorile de formular, faceți următoarele:
<form #myForm="ngForm" countryCity telephoneNumbers > <show-errors [control]="myForm"></show-errors> .. </form>
Ultimul lucru rămas este validarea pentru un nume unic. Acest lucru este puțin diferit; pentru a verifica dacă numele este unic, cel mai probabil este nevoie de un apel către back-end pentru a verifica toate numele existente. Aceasta se clasifică drept operație asincronă. În acest scop, putem reutiliza tehnica anterioară pentru validatorii personalizați, doar să facem ca validate
să returneze un obiect care va fi rezolvat cândva în viitor (promisiune sau un observabil). În cazul nostru, vom folosi o promisiune:
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); }); } }
Așteptăm 1 secundă și apoi returnăm un rezultat. Similar cu validatorii de sincronizare, dacă promisiunea este rezolvată cu null
, înseamnă că validarea a trecut; dacă promisiunea este rezolvată cu orice altceva, atunci validarea a eșuat. De asemenea, observați că acum acest validator este înregistrat la un alt furnizor multiplu, NG_ASYNC_VALIDATORS
. O proprietate utilă a formularelor referitoare la validatorii asincron este proprietatea pending
. Poate fi folosit astfel:
<button [disabled]="myForm.pending">Register</button>
Acesta va dezactiva butonul până când validatoarele asincrone sunt rezolvate.
Iată un Plunker care conține întregul AppComponent, componenta ShowErrors și toți validatorii.
Cu aceste exemple, am acoperit majoritatea cazurilor de lucru cu formulare bazate pe șablon. Am arătat că formularele bazate pe șabloane sunt într-adevăr similare cu formularele din AngularJS (va fi foarte ușor pentru dezvoltatorii AngularJS să migreze). Cu acest tip de formular, este destul de ușor să integrați formularele Angular 4 cu o programare minimă, în principal cu manipulări în șablonul HTML.
Forme reactive
Formele reactive erau cunoscute și sub denumirea de forme „conduse pe model”, dar îmi place să le numesc forme „programatice”, și în curând veți vedea de ce. Formularele reactive reprezintă o nouă abordare față de suportul formularelor Angular 4, așa că, spre deosebire de modelele bazate pe șablon, dezvoltatorii AngularJS nu vor fi familiarizați cu acest tip.
Putem începe acum, vă amintiți cum formularele bazate pe șablon aveau un modul special? Ei bine, formularele reactive au și un modul propriu, numit ReactiveFormsModule și trebuie importate pentru a activa acest tip de formulare.
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 {}
De asemenea, nu uitați să porniți aplicația.
Putem începe cu același AppComponent și șablon ca în secțiunea anterioară.
În acest moment, dacă FormsModule nu este importat (și vă rugăm să vă asigurați că nu este), avem doar un element de formular HTML obișnuit cu câteva controale de formular, fără magie unghiulară aici.
Ajungem la punctul în care veți observa de ce îmi place să numesc această abordare „programatică”. Pentru a activa formularele Angular 4, trebuie să declarăm manual obiectul FormGroup și să-l populam cu controale ca aceasta:

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); } }
Metodele printForm
și register
sunt aceleași din exemplele anterioare și vor fi utilizate în următorii pași. Tipurile de cheie folosite aici sunt FormGroup, FormControl și FormArray. Aceste trei tipuri sunt tot ce avem nevoie pentru a crea un FormGroup valid. FormGroup este ușor; este un simplu container de controale. FormControl este, de asemenea, ușor; este orice control (de exemplu, intrare). Și, în sfârșit, FormArray este piesa puzzle-ului care ne lipsea în abordarea bazată pe șabloane. FormArray permite menținerea unui grup de controale fără a specifica o cheie concretă pentru fiecare control, practic o serie de controale (pare lucrul perfect pentru numerele de telefon, nu?).
Când construiți oricare dintre aceste trei tipuri, amintiți-vă această regulă de 3. Constructorul pentru fiecare tip primește trei argumente: value
, validator sau listă de validatori și validator asincron sau listă de validatori asincron, definite în cod:
constructor(value: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]);
Pentru FormGroup, value
este un obiect în care fiecare cheie reprezintă numele unui control, iar valoarea este controlul însuși.
Pentru FormArray, value
este o matrice de controale.
Pentru FormControl, value
este valoarea inițială sau starea inițială (obiect care conține o value
și o proprietate disabled
) a controlului.
Am creat obiectul FormGroup, dar șablonul încă nu cunoaște acest obiect. Legătura dintre FormGroup din componentă și șablon se face cu patru directive: formGroup
, formControlName
, formGroupName
și formArrayName
, folosite astfel:
<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>
Acum că avem FormArray, puteți vedea că putem folosi acea structură pentru redarea tuturor numerelor de telefon.
Și acum pentru a adăuga suport pentru adăugarea și eliminarea numerelor de telefon (în componentă):
remove(i: number) { (<FormArray>this.myForm.get('phoneNumbers')).removeAt(i); } add() { (<FormArray>this.myForm.get('phoneNumbers')).push(new FormControl('')); }
Acum, avem o formă reactivă Angular 4 complet funcțională. Observați diferența față de formularele bazate pe șablon în care FormGroup a fost „creat în șablon” (prin scanarea structurii șablonului) și trecut la componentă, în formele reactive este invers, întregul FormGroup este creat în componentă, apoi „transmis la șablon” și legat cu controalele corespunzătoare. Dar, din nou, avem aceeași problemă cu validarea, problemă care va fi rezolvată în secțiunea următoare.
Validare
Când vine vorba de validare, formele reactive sunt mult mai flexibile decât formele bazate pe șablon. Fără modificări suplimentare, putem reutiliza aceleași validatoare care au fost implementate anterior (pentru modelele bazate pe șablon). Deci, prin adăugarea directivelor validatorului, putem activa aceeași validare:
<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>
Rețineți că acum nu avem directiva NgModel pe care să o transmitem la componenta ShowErrors, dar întregul FormGroup este deja construit și putem trece AbstractControl corect pentru preluarea erorilor.
Iată un Plunker complet funcțional cu acest tip de validare pentru formele reactive.
Dar nu ar fi distractiv dacă am reutiliza validatorii, nu? Vom arunca o privire asupra modului de a specifica validatorii la crearea grupului de formulare.
Vă amintiți regula „regula 3s” pe care am menționat-o despre constructorul pentru FormGroup, FormControl și FormArray? Da, am spus că constructorul poate primi funcții de validare. Deci, hai să încercăm această abordare.
În primul rând, trebuie să extragem funcțiile de validate
ale tuturor validatorilor într-o clasă care le expune ca metode statice:
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]) );
Vedea? 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?
Odată ce știi asta, gândește-te la ce facem dacă adunăm starea formelor. Așa este, datele sunt împinse în sus în arborele componente. Chiar și atunci când lucrați cu formulare cu un singur nivel, integrarea controalelor de formular ( ngModel
) și a formularului în sine nu este atât de plăcută. Acestea declanșează un ciclu suplimentar de detectare a modificărilor la înregistrarea sau actualizarea valorii unui control (se face cu utilizarea unei promisiuni rezolvate, dar păstrează secretul). De ce este nevoie de o rundă suplimentară? Ei bine, din același motiv, datele curg în sus, de la control la formular. Dar, poate, uneori, imbricarea formelor pe mai multe componente este o caracteristică necesară și trebuie să ne gândim la o soluție pentru a susține această cerință.
Din ceea ce știm până acum, prima idee care ne vine în minte este utilizarea formelor reactive, crearea arborelui complet al formei într-o componentă rădăcină și apoi trecerea formularelor copil componentelor copil ca intrări. În acest fel, ați cuplat strâns părintele cu componentele copil și ați aglomerat logica de afaceri a componentei rădăcină cu gestionarea creării tuturor formelor copil. Haide, suntem profesioniști, sunt sigur că putem găsi o modalitate de a crea componente total izolate cu formulare și de a oferi o modalitate prin care formularul să propagă starea oricui este părintele.
Acestea fiind spuse, iată o directivă care permite imbricarea formelor Angular 4 (implementată pentru că era necesară pentru un proiect):
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()); } }
Exemplul din următorul GIF arată o componentă main
care conține form-1
și, în interiorul acelui formular, există o altă componentă imbricată, component-2
. component-2
conține form-2
, care are form-2.1
, form-2.2
și o componentă ( component-3
) care are un arbore de formă reactivă în ea și o componentă ( component-4
) care conține o formă care este izolat de toate celelalte forme. Destul de dezordonat, știu, dar am vrut să fac un scenariu destul de complex pentru a arăta funcționalitatea acestei directive.
Exemplul este implementat în acest Plunker.
Caracteristicile pe care le oferă sunt:
Activează imbricarea prin adăugarea directivei nestableForm la elemente: form, ngForm, [ngForm], [formGroup]
Funcționează cu formulare bazate pe șablon și reactive
Permite construirea unui arbore de forme care se întinde pe mai multe componente
Izolează un sub-arboresc de formulare cu rootNestableForm="true" (nu se va înregistra la părintele nestableForm)
Această directivă permite unui formular dintr-o componentă fiu să se înregistreze la primul formular nestableForm părinte, indiferent dacă formularul părinte este declarat în aceeași componentă sau nu. Vom intra în detaliile implementării.
În primul rând, să aruncăm o privire la constructor. Primul argument este:
@SkipSelf() @Optional() private parentForm: NestableFormDirective
Acesta caută primul părinte NestableFormDirective. @SkipSelf, pentru a nu se potrivi, și @Opțional deoarece este posibil să nu găsească un părinte, în cazul formei rădăcină. Acum avem o referință la forma parentală imbricabilă.
Al doilea argument este:
private injector: Injector
Injectorul este utilizat pentru a prelua provider
curent FormGroup (șablon sau reactiv).
Iar ultimul argument este:
@Attribute('rootNestableForm') private isRoot
pentru a obține valoarea care determină dacă această formă este izolată din arborele formelor.
Apoi, pe ngInit
ca acțiune amânată (vă amintiți fluxul invers de date?), FormGroup-ul curent este rezolvat, un nou control FormArray numit CHILD_FORMS
este înregistrat în acest FormGroup (unde vor fi înregistrate formularele copil) și ca ultima acțiune, FormGroup-ul curent este înregistrat ca copil în formularul încadrabil pentru părinte.
Acțiunea ngOnDestroy
este executată când formularul este distrus. La distrugere, din nou ca acțiune amânată, formularul curent este eliminat de la părinte (de-înregistrare).
Directiva pentru formularele imbricate poate fi personalizată în continuare pentru o anumită nevoie — poate elimina suportul pentru formularele reactive, înregistrează fiecare formular copil sub un nume specific (nu într-o matrice CHILD_FORMS) și așa mai departe. Această implementare a directivei nestableForm a îndeplinit cerințele proiectului și este prezentată aici ca atare. Acesta acoperă câteva cazuri de bază, cum ar fi adăugarea unui formular nou sau eliminarea dinamică a unui formular existent (*ngIf) și propagarea stării formularului către părinte. Acest lucru se rezumă, practic, la operațiuni care pot fi rezolvate într-un singur ciclu de detectare a modificării (cu amânare sau nu).
Dacă doriți un scenariu mai avansat, cum ar fi adăugarea unei validări condiționate la o intrare (de exemplu, [required] = „someCondition”) care ar necesita 2 runde de detectare a modificării, nu va funcționa din cauza regulii „rezoluție-un ciclu de detectare” impus de Angular 4.
Oricum, dacă intenționați să utilizați această directivă sau să implementați o altă soluție, fiți foarte atenți la lucrurile care au fost menționate legate de detectarea modificărilor. În acest moment, așa este implementat Angular 4. S-ar putea schimba în viitor - nu putem ști. Configurarea actuală și restricția impusă în Angular 4 care au fost menționate în acest articol ar putea fi un dezavantaj sau un avantaj. Rămâne de văzut.
Forme simplificate cu Angular 4
După cum puteți vedea, echipa Angular a făcut o treabă foarte bună oferind multe funcționalități legate de formulare. Sper că această postare va servi drept ghid complet pentru lucrul cu diferitele tipuri de formulare din Angular 4, oferind, de asemenea, o perspectivă asupra unor concepte mai avansate, cum ar fi imbricarea formularelor și procesul de detectare a schimbărilor.
În ciuda tuturor postărilor diferite legate de formularele Angular 4 (sau orice alt subiect Angular 4 de altfel), în opinia mea, cel mai bun punct de plecare este documentația oficială Angular 4. De asemenea, băieții Angular au o documentație frumoasă în codul lor. De multe ori, am găsit o soluție doar privind codul lor sursă și documentația de acolo, fără Google sau altceva. Despre imbricarea formularelor, discutată în ultima secțiune, cred că orice dezvoltator AngularJS care începe să învețe Angular 4 se va împiedica la un moment dat de această problemă, ceea ce a fost un fel de inspirație pentru a scrie această postare.
După cum am văzut de asemenea, există două tipuri de formulare și nu există o regulă strictă conform căreia nu le puteți folosi împreună. Este frumos să păstrați baza de cod curată și consecventă, dar uneori, ceva se poate face mai ușor cu formularele bazate pe șabloane și, uneori, este invers. Așadar, dacă nu vă deranjează dimensiunile pachetelor puțin mai mari, vă sugerez să folosiți tot ce considerați mai potrivit de la caz la caz. Doar nu le amestecați în aceeași componentă, deoarece probabil va duce la o confuzie.
Plunkers utilizate în această postare
Formulare bazate pe șabloane
Forme reactive, validatoare de șabloane
Forme reactive, validatoare de cod
Forme reactive, generator de forme
Formulare bazate pe șabloane, imbricate în aceeași componentă
Forme reactive, imbricate în aceeași componentă
Forme imbricate prin arborele componentelor