Praca z formularzami Angular 4: zagnieżdżanie i walidacja danych wejściowych

Opublikowany: 2022-03-11

W sieci niektóre z najwcześniejszych elementów wprowadzania danych przez użytkownika to przycisk, pole wyboru, wprowadzanie tekstu i przyciski radiowe. Do dziś elementy te są nadal używane w nowoczesnych aplikacjach internetowych, mimo że standard HTML przeszedł długą drogę od swojej wczesnej definicji i umożliwia teraz wszelkiego rodzaju wymyślne interakcje.

Weryfikowanie danych wejściowych użytkownika jest istotną częścią każdej niezawodnej aplikacji internetowej.

Formularze w aplikacjach Angular mogą agregować stan wszystkich danych wejściowych znajdujących się w tym formularzu i zapewniać ogólny stan, taki jak stan weryfikacji pełnego formularza. Może to być bardzo przydatne przy podejmowaniu decyzji, czy dane wejściowe użytkownika zostaną zaakceptowane, czy odrzucone bez sprawdzania każdego z nich osobno.

Walidacja danych wejściowych dla formularzy kątowych 4

W tym artykule dowiesz się, jak pracować z formularzami i z łatwością przeprowadzać walidację formularzy w aplikacji Angular.

W Angular 4 dostępne są dwa różne typy formularzy, z którymi można pracować: formularze oparte na szablonach i formularze reaktywne. Przejrzymy każdy typ formularza, używając tego samego przykładu, aby zobaczyć, jak te same rzeczy można zaimplementować na różne sposoby. W dalszej części artykułu przyjrzymy się nowatorskiemu podejściu do konfigurowania formularzy zagnieżdżonych i pracy z nimi.

Formy kątowe 4

W Angular 4 następujące cztery statusy są powszechnie używane przez formularze:

  • valid – stan ważności wszystkich kontrolek formularza, prawda, jeśli wszystkie kontrolki są poprawne

  • nieważny – odwrotność valid ; prawda, jeśli jakaś kontrola jest nieprawidłowa

  • nieskazitelny – nadaje status o „czystości” formy; prawda, jeśli żadna kontrolka nie została zmodyfikowana

  • brudny – odwrotność pristine ; prawda, jeśli jakaś kontrolka została zmodyfikowana

Rzućmy okiem na podstawowy przykład formularza:

 <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>

Specyfikacja tego przykładu jest następująca:

  • nazwa - jest wymagana i unikalna wśród wszystkich zarejestrowanych użytkowników

  • rok urodzenia - powinien być prawidłowym numerem, a użytkownik musi mieć co najmniej 18 lat i mniej niż 85 lat

  • kraj - jest obowiązkowy, a żeby trochę skomplikować, potrzebujemy walidacji, że jeśli krajem jest Francja, to miastem musi być Paryż (załóżmy, że nasza usługa jest oferowana tylko w Paryżu)

  • phoneNumber – każdy numer telefonu musi mieć określony wzór, musi istnieć co najmniej jeden numer telefonu, a użytkownik może dodać nowy lub usunąć istniejący numer telefonu.

  • Przycisk „Zarejestruj” jest aktywny tylko wtedy, gdy wszystkie dane wejściowe są prawidłowe i po kliknięciu przesyła formularz.

  • Polecenie "Drukuj do konsoli" drukuje po prostu wartość wszystkich danych wejściowych do konsoli po kliknięciu.

Ostatecznym celem jest pełne wdrożenie zdefiniowanej specyfikacji.

Formularze oparte na szablonach

Formularze oparte na szablonach są bardzo podobne do formularzy w AngularJS (lub Angular 1, jak niektórzy to nazywają). Tak więc ktoś, kto pracował z formularzami w AngularJS, będzie dobrze zaznajomiony z tym podejściem do pracy z formularzami.

Wraz z wprowadzeniem modułów w Angular 4 wymusza się, aby każdy konkretny typ formularza znajdował się w osobnym module i musimy wyraźnie określić, jakiego typu będziemy używać, importując odpowiedni moduł. Ten moduł dla formularzy opartych na szablonie to FormsModule. Mając to na uwadze, możesz aktywować formularze oparte na szablonach w następujący sposób:

 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 {}

Jak przedstawiono w tym fragmencie kodu, najpierw musimy zaimportować moduł przeglądarki, ponieważ „zapewnia usługi niezbędne do uruchomienia i uruchomienia aplikacji przeglądarki”. (z dokumentacji Angulara 4). Następnie importujemy wymagany moduł FormsModule, aby aktywować formularze oparte na szablonie. I na koniec deklaracja komponentu głównego, AppComponent, gdzie w kolejnych krokach zaimplementujemy formularz.

Pamiętaj, że w tym przykładzie i w następnych przykładach musisz upewnić się, że aplikacja jest poprawnie ładowana za pomocą metody platformBrowserDynamic .

 import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {AppModule} from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);

Możemy założyć, że nasz AppComponent (app.component.ts) wygląda mniej więcej tak:

 import {Component} from '@angular/core' @Component({ selector: 'my-app', templateUrl: 'src/app.component.tpl.html' }) export class AppComponent { }

Gdzie szablon tego komponentu znajduje się w app.component.tpl.html i możemy skopiować początkowy szablon do tego pliku.

Zwróć uwagę, że każdy element wejściowy musi mieć atrybut name , aby mógł być poprawnie zidentyfikowany w formularzu. Chociaż wygląda to na prosty formularz HTML, zdefiniowaliśmy już formularz obsługiwany przez Angular 4 (może jeszcze go nie widzisz). Kiedy FormsModule jest importowany, Angular 4 automatycznie wykrywa element HTML form i dołącza do niego komponent NgForm (za pomocą selector komponentu NgForm). Tak jest w naszym przykładzie. Chociaż ten formularz Angular 4 jest zadeklarowany, w tym momencie nie są znane żadne dane wejściowe obsługiwane w Angular 4. Angular 4 nie jest tak inwazyjny, aby zarejestrować każdy input element HTML do najbliższego przodka form .

Kluczem umożliwiającym zauważenie elementu wejściowego jako elementu Angular 4 i zarejestrowanie go w komponencie NgForm jest dyrektywa NgModel. Możemy więc rozszerzyć szablon app.component.tpl.html w następujący sposób:

 <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>

Dzięki dodaniu dyrektywy NgModel wszystkie dane wejściowe są rejestrowane w komponencie NgForm. Dzięki temu zdefiniowaliśmy w pełni działający formularz Angular 4 i jak dotąd jest tak dobry, ale nadal nie mamy sposobu na dostęp do komponentu NgForm i oferowanych przez niego funkcji. Dwie główne funkcjonalności oferowane przez NgForm to:

  • Pobieranie wartości wszystkich zarejestrowanych kontrolek wejściowych

  • Pobieranie ogólnego stanu wszystkich elementów sterujących

Aby odsłonić NgForm, możemy dodać do elementu <form>:

 <form #myForm="ngForm"> .. </form>

Jest to możliwe dzięki właściwości exportAs dekoratora Component .

Gdy to zrobimy, możemy uzyskać dostęp do wartości wszystkich kontrolek wejściowych i rozszerzyć szablon do:

 <form #myForm="ngForm"> .. <pre>{{myForm.value | json}}</pre> </form>

Za pomocą myForm.value uzyskujemy dostęp do danych JSON zawierających wartości wszystkich zarejestrowanych danych wejściowych, a za pomocą {{myForm.value | json}} {{myForm.value | json}} , ładnie drukujemy JSON z wartościami.

A co jeśli chcemy mieć podgrupę danych wejściowych z określonego kontekstu opakowaną w kontener i oddzielny obiekt w wartościach JSON np. lokalizacja zawierająca kraj i miasto lub numery telefonów? Nie stresuj się — formularze oparte na szablonach w Angular 4 również to obejmują. Sposobem na osiągnięcie tego jest użycie dyrektywy ngModelGroup .

 <form #myForm="ngForm"> .. <div ngModelGroup="location"> .. </div> </div ngModelGroup="phoneNumbers"> .. <div> .. </form>

Brakuje nam teraz sposobu na dodanie wielu numerów telefonów. Najlepszym sposobem na to byłoby użycie tablicy, jako najlepszej reprezentacji iterowalnego kontenera wielu obiektów, ale w chwili pisania tego artykułu ta funkcja nie jest zaimplementowana dla formularzy opartych na szablonach. Dlatego musimy zastosować obejście, aby to zadziałało. Sekcja numerów telefonów musi zostać zaktualizowana w następujący sposób:

 <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() służy do touched formularza, dzięki czemu możemy wyświetlić błędy w tym momencie. Przyciski nie aktywują tej właściwości po kliknięciu, tylko dane wejściowe. Aby kolejne przykłady były bardziej przejrzyste, nie dodam tego wiersza w module obsługi kliknięć dla add() i remove() . Wyobraź sobie, że tam jest. (Jest obecny w Plunkers.)

Musimy również zaktualizować AppComponent , aby zawierał następujący kod:

 private count:number = 1; phoneNumberIds:number[] = [1]; remove(i:number) { this.phoneNumberIds.splice(i, 1); } add() { this.phoneNumberIds.push(++this.count); }

Musimy przechowywać unikalny identyfikator dla każdego dodawanego numeru telefonu, a w *ngFor śledzić kontrolki numeru telefonu według ich identyfikatora (przyznaję, że nie jest to zbyt miłe, ale obawiam się, że dopóki zespół Angular 4 nie zaimplementuje tej funkcji , to najlepsze co możemy zrobić)

Ok, co mamy do tej pory, dodaliśmy formularz obsługiwany przez Angular 4 z danymi wejściowymi, dodaliśmy określone grupowanie danych wejściowych (lokalizacja i numery telefonów) i udostępniliśmy formularz w szablonie. Ale co, jeśli chcielibyśmy uzyskać dostęp do obiektu NgForm za pomocą jakiejś metody w komponencie? Przyjrzymy się dwóm sposobom na zrobienie tego.

W pierwszym przypadku, NgForm, oznaczony w bieżącym przykładzie myForm , może zostać przekazany jako argument do funkcji, która będzie służyć jako moduł obsługi zdarzenia onSubmit formularza. Dla lepszej integracji, zdarzenie onSubmit jest wrapped zdarzeniem specyficznym dla Angulara 4 o nazwie ngSubmit , i jest to właściwy sposób postępowania, jeśli chcemy wykonać jakąś akcję przy wysłaniu. Tak więc przykład będzie teraz wyglądał tak:

 <form #myForm="ngForm" (ngSubmit)="register(myForm)"> … </form>

Musimy mieć odpowiednią metodę register , zaimplementowaną w AppComponent. Coś jak:

 register (myForm: NgForm) { console.log('Successful registration'); console.log(myForm); }

W ten sposób, wykorzystując zdarzenie onSubmit, mamy dostęp do komponentu NgForm tylko wtedy, gdy wykonywane jest wysyłanie.

Drugim sposobem jest użycie zapytania widoku poprzez dodanie dekoratora @ViewChild do właściwości komponentu.

 @ViewChild('myForm') private myForm: NgForm;

Dzięki takiemu podejściu mamy dostęp do formularza niezależnie od tego, czy zdarzenie onSubmit zostało wywołane, czy nie.

Świetnie! Teraz mamy w pełni działający formularz Angular 4 z dostępem do formularza w komponencie. Ale czy zauważyłeś, że czegoś brakuje? Co się stanie, jeśli użytkownik wpisze coś w stylu „to nie jest rok” w polu „lata”? Tak, rozumiesz, brakuje nam walidacji danych wejściowych i omówimy to w następnej sekcji.

Walidacja

Walidacja jest bardzo ważna dla każdej aplikacji. Zawsze chcemy sprawdzić poprawność danych wprowadzonych przez użytkownika (nie możemy mu ufać), aby zapobiec wysyłaniu/zapisywaniu nieprawidłowych danych i musimy pokazać jakiś znaczący komunikat o błędzie, aby właściwie poprowadzić użytkownika do wprowadzenia prawidłowych danych.

Aby jakaś reguła walidacji była egzekwowana na niektórych danych wejściowych, odpowiedni walidator musi być powiązany z tymi danymi wejściowymi. Angular 4 oferuje już zestaw popularnych walidatorów, takich jak: required , maxLength , minLength

Jak więc możemy powiązać walidator z danymi wejściowymi? Cóż, całkiem proste; po prostu dodaj dyrektywę walidatora do kontrolki:

 <input name="name" ngModel required/>

W tym przykładzie wprowadzenie „nazwa” jest obowiązkowe. Dodajmy kilka walidacji do wszystkich danych wejściowych w naszym przykładzie.

 <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>

Uwaga: novalidate służy do wyłączania sprawdzania poprawności formularza natywnego przeglądarki.

Zrobiliśmy wymaganie „nazwisko”, pole „lata” jest wymagane i musi zawierać tylko cyfry, wymagane jest wprowadzenie kraju i wymagany jest numer telefonu. Ponadto wypisujemy status ważności formularza za pomocą {{myForm.valid}} .

Ulepszeniem tego przykładu byłoby również pokazanie, co jest nie tak z danymi wejściowymi użytkownika (nie tylko pokazanie ogólnego stanu). Zanim przejdziemy do dodawania dodatkowej walidacji, chciałbym zaimplementować komponent pomocniczy, który pozwoli nam wydrukować wszystkie błędy dla dostarczonej kontrolki.

 // 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 z błędami jest wyświetlana tylko wtedy, gdy istnieją jakieś błędy, a wejście jest dotknięte lub zabrudzone.

Wiadomość dla każdego błędu jest sprawdzana na mapie predefiniowanych wiadomości errorMessages (wszystkie wiadomości dodałem z góry).

Ten składnik może być używany w następujący sposób:

 <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>

Musimy udostępnić NgModel dla każdego wejścia i przekazać go do komponentu, który renderuje wszystkie błędy. Możesz zauważyć, że w tym przykładzie użyliśmy wzorca do sprawdzenia, czy dane są liczbą; co jeśli użytkownik wpisze „0000”? To byłby nieprawidłowy wpis. Ponadto brakuje nam walidatorów dla unikalnej nazwy, dziwnego ograniczenia kraju (jeśli kraj='Francja', to miasto musi być 'Paryż'), wzorca dla poprawnego numeru telefonu i walidacji, że co najmniej jeden numer telefonu istnieje. To właściwy czas, aby przyjrzeć się niestandardowym walidatorom.

Angular 4 oferuje interfejs, który musi zaimplementować każdy niestandardowy walidator, interfejs Validatora (co za niespodzianka!). Interfejs Validatora zasadniczo wygląda tak:

 export interface Validator { validate(c: AbstractControl): ValidationErrors | null; registerOnValidatorChange?(fn: () => void): void; }

Tam, gdzie każde konkretne wdrożenie MUSI wdrażać metodę „walidacji”. Ta metoda validate jest naprawdę interesująca, jeśli chodzi o to, co można odebrać jako dane wejściowe, a co należy zwrócić jako dane wyjściowe. Dane wejściowe to AbstractControl, co oznacza, że ​​argument może być dowolnym typem, który rozszerza AbstractControl (FormGroup, FormControl i FormArray). Dane wyjściowe metody validate powinny mieć null lub undefined (brak danych wyjściowych), jeśli dane wejściowe użytkownika są prawidłowe, lub zwrócić obiekt ValidationErrors , jeśli dane wejściowe użytkownika są nieprawidłowe. Mając tę ​​wiedzę, teraz birthYear niestandardowy walidator roku urodzenia.

 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; } }

Jest tu kilka rzeczy do wyjaśnienia. Po pierwsze możesz zauważyć, że zaimplementowaliśmy interfejs Validatora. Metoda validate sprawdza, czy użytkownik ma od 18 do 85 lat przed wprowadzonym rokiem urodzenia. Jeśli dane wejściowe są prawidłowe, zwracana jest null lub zwracany jest obiekt zawierający komunikat sprawdzania poprawności. A ostatnią i najważniejszą częścią jest ogłoszenie tej dyrektywy jako Walidatora. Odbywa się to w parametrze „providers” dekoratora @Directive. Ten walidator jest dostarczany jako jedna wartość wielodostawcy NG_VALIDATORS. Nie zapomnij również zadeklarować tej dyrektywy w NgModule. A teraz możemy użyć tego walidatora w następujący sposób:

 <input type="text" name="birthYear" #year="ngModel" ngModel required birthYear/>

Tak, tak proste!

W przypadku numeru telefonu możemy zweryfikować format numeru telefonu w następujący sposób:

 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; } }

Teraz przyjdź dwa walidacje, dla kraju i liczby numerów telefonów. Zauważasz coś wspólnego dla nich obu? Oba wymagają więcej niż jednego elementu sterującego, aby przeprowadzić prawidłową walidację. Pamiętasz interfejs Validatora i co o nim mówiliśmy? Argumentem metody validate jest AbstractControl, który może być danymi wejściowymi użytkownika lub samym formularzem. Stwarza to możliwość zaimplementowania walidatora, który używa wielu kontrolek do określenia konkretnego stanu walidacji.

 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; } } }

Wdrożyliśmy nowy walidator, walidator kraj-miasto. Możesz zauważyć, że teraz jako argument metoda walidacji otrzymuje FormGroup iz tej grupy możemy pobrać dane wejściowe wymagane do walidacji. Reszta rzeczy jest bardzo podobna do walidatora pojedynczego wejścia.

Walidator liczby numerów telefonów będzie wyglądał tak:

 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; } }

Możemy ich użyć tak:

 <form #myForm="ngForm" countryCity telephoneNumbers> .. </form>

Tak samo jak walidatory danych wejściowych, prawda? Właśnie teraz zastosowałem się do formularza.

Czy pamiętasz komponent ShowErrors? Zaimplementowaliśmy go do pracy z AbstractControlDirective, co oznacza, że ​​możemy go ponownie użyć, aby wyświetlić również wszystkie błędy związane bezpośrednio z tym formularzem. Należy pamiętać, że w tym momencie jedynymi bezpośrednio powiązanymi regułami walidacji z formularzem są Country-city i Telephone numbers (pozostałe walidatory są powiązane z określonymi kontrolkami formularza). Aby wydrukować wszystkie błędy formularza, wykonaj następujące czynności:

 <form #myForm="ngForm" countryCity telephoneNumbers > <show-errors [control]="myForm"></show-errors> .. </form>

Ostatnią rzeczą, która pozostała, jest walidacja unikalnej nazwy. To jest trochę inne; aby sprawdzić, czy nazwa jest unikalna, najprawdopodobniej potrzebne jest wywołanie zaplecza, aby sprawdzić wszystkie istniejące nazwy. To jest klasyfikowane jako operacja asynchroniczna. W tym celu możemy ponownie wykorzystać poprzednią technikę dla niestandardowych walidatorów, wystarczy, że validate zwróci obiekt, który zostanie rozwiązany w przyszłości (obietnica lub obserwowalny). W naszym przypadku skorzystamy z obietnicy:

 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); }); } }

Czekamy 1 sekundę, a następnie zwracamy wynik. Podobnie jak w przypadku walidatorów synchronizacji, jeśli obietnica zostanie rozwiązana za pomocą null , oznacza to, że weryfikacja przeszła; jeśli obietnica zostanie rozwiązana za pomocą czegokolwiek innego, weryfikacja nie powiodła się. Zauważ również, że teraz ten walidator jest zarejestrowany u innego dostawcy, NG_ASYNC_VALIDATORS . Jedną z przydatnych właściwości formularzy dotyczących walidatorów asynchronicznych jest właściwość pending . Może być używany w ten sposób:

 <button [disabled]="myForm.pending">Register</button>

Spowoduje to wyłączenie przycisku, dopóki walidatory asynchroniczne nie zostaną rozwiązane.

Oto Plunker zawierający kompletny AppComponent, komponent ShowErrors i wszystkie walidatory.

W tych przykładach omówiliśmy większość przypadków pracy z formularzami opartymi na szablonach. Pokazaliśmy, że formularze oparte na szablonach są bardzo podobne do formularzy w AngularJS (migracja będzie bardzo łatwa dla programistów AngularJS). Z tego typu formularzem dość łatwo jest zintegrować formularze Angular 4 z minimalnym programowaniem, głównie z manipulacjami w szablonie HTML.

Formularze reaktywne

Formularze reaktywne były również znane jako formularze „oparte na modelu”, ale lubię nazywać je formularzami „zautomatyzowanymi” i wkrótce zobaczysz, dlaczego. Formularze reaktywne to nowe podejście do obsługi formularzy Angular 4, więc w przeciwieństwie do szablonów, programiści AngularJS nie będą zaznajomieni z tym typem.

Możemy zacząć już teraz, pamiętasz jak formularze oparte na szablonach miały specjalny moduł? Cóż, formularze reaktywne również mają swój własny moduł o nazwie ReactiveFormsModule i muszą zostać zaimportowane, aby aktywować tego typu formularze.

 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 {}

Nie zapomnij też załadować aplikacji.

Możemy zacząć od tego samego AppComponent i szablonu, co w poprzedniej sekcji.

W tym momencie, jeśli FormsModule nie jest importowany (i upewnij się, że tak nie jest), mamy tylko zwykły element formularza HTML z kilkoma kontrolkami formularza, bez magii Angulara.

Dochodzimy do momentu, w którym zauważysz, dlaczego lubię nazywać to podejście „zautomatyzowanym”. Aby włączyć formularze Angular 4, musimy ręcznie zadeklarować obiekt FormGroup i wypełnić go kontrolkami w następujący sposób:

 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); } }

Metody printForm i register są takie same jak w poprzednich przykładach i zostaną użyte w następnych krokach. Używane tutaj typy kluczy to FormGroup, FormControl i FormArray. Te trzy typy to wszystko, czego potrzebujemy, aby utworzyć poprawną grupę FormGroup. FormGroup jest łatwe; jest to prosty kontener kontrolek. FormControl jest również łatwy; jest to dowolna kontrola (np. wejście). I wreszcie, FormArray to element układanki, którego brakowało nam w podejściu opartym na szablonach. FormArray pozwala na utrzymanie grupy kontrolek bez określania konkretnego klucza dla każdej kontrolki, w zasadzie tablicy kontrolek (wygląda na to, że jest to idealne rozwiązanie dla numerów telefonów, prawda?).

Konstruując którykolwiek z tych trzech typów, pamiętaj o zasadzie trójek. Konstruktor dla każdego typu otrzymuje trzy argumenty — value , walidator lub listę walidatorów oraz walidator async lub listę walidatorów async, zdefiniowane w kodzie:

 constructor(value: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]);

W przypadku FormGroup value jest obiektem, w którym każdy klucz reprezentuje nazwę kontrolki, a wartość jest samą kontrolką.

W przypadku FormArray value jest tablicą kontrolek.

W przypadku FormControl value jest wartością początkową lub stanem początkowym (obiekt zawierający value i disabled właściwość) kontrolki.

Utworzyliśmy obiekt FormGroup, ale szablon nadal nie jest świadomy tego obiektu. Łączenie między FormGroup w komponencie a szablonem odbywa się za pomocą czterech dyrektyw: formGroup , formControlName , formGroupName i formArrayName , używanych w następujący sposób:

 <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>

Teraz, gdy mamy FormArray, widać, że możemy użyć tej struktury do renderowania wszystkich numerów telefonów.

A teraz dodać obsługę dodawania i usuwania numerów telefonów (w komponencie):

 remove(i: number) { (<FormArray>this.myForm.get('phoneNumbers')).removeAt(i); } add() { (<FormArray>this.myForm.get('phoneNumbers')).push(new FormControl('')); }

Teraz mamy w pełni działającą formę reaktywną Angular 4. Zwróć uwagę na różnicę w stosunku do formularzy opartych na szablonie, w których FormGroup został „utworzony w szablonie” (poprzez skanowanie struktury szablonu) i przekazany do komponentu, w formularzach reaktywnych jest odwrotnie, kompletny FormGroup jest tworzony w komponent, a następnie „przekazywane do szablonu” i łączone z odpowiednimi kontrolkami. Ale znowu mamy ten sam problem z walidacją, problem, który zostanie rozwiązany w następnej sekcji.

Walidacja

Jeśli chodzi o walidację, formularze reaktywne są znacznie bardziej elastyczne niż formularze oparte na szablonach. Bez dodatkowych zmian możemy ponownie wykorzystać te same walidatory, które zostały zaimplementowane wcześniej (dla szablonu opartego na szablonie). Tak więc, dodając dyrektywy walidatora, możemy aktywować tę samą walidację:

 <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>

Należy pamiętać, że teraz nie mamy dyrektywy NgModel do przekazania do komponentu ShowErrors, ale cała FormGroup jest już skonstruowana i możemy przekazać poprawny AbstractControl w celu odzyskania błędów.

Oto w pełni działający Plunker z tego rodzaju walidacją dla formularzy reaktywnych.

Ale nie byłoby fajnie, gdybyśmy po prostu ponownie wykorzystali walidatory, prawda? Przyjrzymy się, jak określić walidatory podczas tworzenia grupy formularzy.

Pamiętasz regułę „3s”, o której wspomnieliśmy o konstruktorze dla FormGroup, FormControl i FormArray? Tak, powiedzieliśmy, że konstruktor może otrzymywać funkcje walidatora. Spróbujmy więc tego podejścia.

Najpierw musimy wyodrębnić funkcje validate wszystkich walidatorów do klasy, eksponując je jako metody statyczne:

 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]) );

Widzieć? 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.

A tree of nested components holding a form.

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?

Kiedy już to wiesz, zastanów się, co robimy, gdy agregujemy stan formularzy. Zgadza się, dane są przesuwane w górę drzewa komponentów. Nawet podczas pracy z formularzami jednopoziomowymi integracja kontrolek formularza ( ngModel ) i samego formularza nie jest tak przyjemna. Wyzwalają dodatkowy cykl wykrywania zmian podczas rejestracji lub aktualizacji wartości kontrolki (odbywa się to za pomocą rozwiązanej obietnicy, ale zachowaj to w tajemnicy). Dlaczego potrzebna jest dodatkowa runda? Z tego samego powodu dane płyną w górę, od kontrolki do formularza. Czasami jednak zagnieżdżanie formularzy w wielu komponentach jest wymaganą funkcją i musimy pomyśleć o rozwiązaniu, które spełni ten wymóg.

Z tego, co wiemy do tej pory, pierwszym pomysłem, który przychodzi do głowy, jest użycie formularzy reaktywnych, utworzenie pełnego drzewa formularzy w jakimś głównym komponencie, a następnie przekazanie formularzy potomnych do komponentów potomnych jako danych wejściowych. W ten sposób ściśle powiążesz rodzica z komponentami podrzędnymi i zaśmiecisz logikę biznesową komponentu głównego z obsługą tworzenia wszystkich formularzy podrzędnych. Daj spokój, jesteśmy profesjonalistami, jestem pewien, że możemy wymyślić sposób na tworzenie całkowicie izolowanych komponentów za pomocą formularzy i zapewnić sposób, aby formularz po prostu propagował stan do tego, kto jest rodzicem.

Biorąc to wszystko pod uwagę, oto dyrektywa, która umożliwia zagnieżdżanie formularzy Angular 4 (zaimplementowana, ponieważ była potrzebna w projekcie):

 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()); } }

Przykład w poniższym pliku GIF pokazuje jeden main składnik zawierający form-1 , a wewnątrz tego formularza znajduje się inny składnik zagnieżdżony, component-2 . component-2 zawiera form-2 , który ma zagnieżdżone form-2.1 , form-2.2 i składnik ( component-3 ) zawierający drzewo formularza reaktywnego oraz składnik ( component-4 ) zawierający formularz, który jest odizolowany od wszystkich innych form. Wiem, dość niechlujny, ale chciałem zrobić dość złożony scenariusz, aby pokazać funkcjonalność tej dyrektywy.

Złożony przypadek walidacji formy Angular z wieloma komponentami

Przykład jest zaimplementowany w tym Plunkerze.

Funkcje, które oferuje to:

  • Włącza zagnieżdżanie poprzez dodanie dyrektywy nestableForm do elementów: form, ngForm, [ngForm], [formGroup]

  • Działa z formularzami opartymi na szablonach i reaktywnymi

  • Umożliwia budowanie drzewa formularzy, które obejmuje wiele komponentów

  • Izoluje poddrzewo formularzy z rootNestableForm=”true” (nie zarejestruje się w nadrzędnym nestableForm)

Ta dyrektywa umożliwia zarejestrowanie formularza w komponencie potomnym do pierwszego rodzica nestableForm, niezależnie od tego, czy formularz nadrzędny jest zadeklarowany w tym samym komponencie, czy nie. Zajmiemy się szczegółami realizacji.

Najpierw spójrzmy na konstruktora. Pierwszy argument to:

 @SkipSelf() @Optional() private parentForm: NestableFormDirective

Spowoduje to wyszukanie pierwszego rodzica NestableFormDirective. @SkipSelf, aby nie pasować do siebie, i @Optional, ponieważ może nie znaleźć rodzica, w przypadku formy głównej. Teraz mamy odniesienie do rodzicielskiej formy zagnieżdżonej.

Drugi argument to:

 private injector: Injector

Wtryskiwacz służy do pobierania aktualnego provider FormGroup (szablonowego lub reaktywnego).

A ostatni argument to:

 @Attribute('rootNestableForm') private isRoot

aby uzyskać wartość określającą, czy ta forma jest wyizolowana z drzewa form.

Następnie, w ngInit jako odroczonej akcji (pamiętasz wsteczny przepływ danych?), bieżący FormGroup jest rozwiązany, nowa kontrolka FormArray o nazwie CHILD_FORMS jest rejestrowana w tej FormGroup (gdzie zostaną zarejestrowane formularze podrzędne) i jako ostatnia akcja, bieżąca FormGroup jest zarejestrowana jako dziecko w nadrzędnym formularzu zagnieżdżonym.

Akcja ngOnDestroy jest wykonywana po zniszczeniu formularza. Po zniszczeniu, ponownie jako odroczona czynność, aktualny formularz jest usuwany z rodzica (wyrejestrowanie).

Dyrektywę dotyczącą formularzy zagnieżdżonych można dodatkowo dostosować do konkretnych potrzeb — na przykład usunąć obsługę formularzy reaktywnych, zarejestrować każdy formularz podrzędny pod określoną nazwą (nie w tablicy CHILD_FORMS) i tak dalej. Ta implementacja dyrektywy nestableForm spełniła wymagania projektu i została tutaj przedstawiona jako taka. Obejmuje kilka podstawowych przypadków, takich jak dodawanie nowego formularza lub dynamiczne usuwanie istniejącego formularza (*ngIf) i propagacja stanu formularza do rodzica. Sprowadza się to w zasadzie do operacji, które można rozwiązać w ciągu jednego cyklu wykrywania zmian (z odroczeniem lub nie).

Jeśli potrzebujesz bardziej zaawansowanego scenariusza, takiego jak dodanie warunkowej walidacji do niektórych danych wejściowych (np. [wymagane] = „jakiś warunek”), które wymagałyby 2 rund wykrywania zmian, nie zadziała z powodu zasady „rozpoznawanie jednego cyklu wykrywania” narzucone przez Angulara 4.

W każdym razie, jeśli planujesz użyć tej dyrektywy lub wdrożyć jakieś inne rozwiązanie, bądź bardzo ostrożny w odniesieniu do wspomnianych rzeczy związanych z wykrywaniem zmian. W tym momencie tak zaimplementowany jest Angular 4. To może się zmienić w przyszłości – nie możemy tego wiedzieć. Obecna konfiguracja i wymuszone ograniczenia w Angular 4, o których wspomniano w tym artykule, mogą być wadą lub korzyścią. To się okaże.

Łatwe tworzenie formularzy dzięki Angular 4

Jak widać, zespół Angulara wykonał naprawdę dobrą robotę dostarczając wiele funkcjonalności związanych z formularzami. Mam nadzieję, że ten post posłuży jako kompletny przewodnik po pracy z różnymi typami formularzy w Angular 4, dając również wgląd w niektóre bardziej zaawansowane koncepcje, takie jak zagnieżdżanie formularzy i proces wykrywania zmian.

Pomimo wszystkich różnych postów związanych z formularzami Angulara 4 (lub jakimkolwiek innym tematem Angulara 4, jeśli o to chodzi), moim zdaniem najlepszym punktem wyjścia jest oficjalna dokumentacja Angulara 4. Poza tym chłopaki z Angulara mają w swoim kodzie niezłą dokumentację. Wiele razy znalazłem rozwiązanie po prostu patrząc na ich kod źródłowy i dokumentację tam, bez Googlingu ani niczego. Jeśli chodzi o zagnieżdżanie formularzy, omówione w ostatniej sekcji, wierzę, że każdy programista AngularJS, który zaczyna uczyć się Angulara 4, w pewnym momencie natknie się na ten problem, co było swoistą inspiracją do napisania tego posta.

Jak również widzieliśmy, istnieją dwa rodzaje formularzy i nie ma ścisłej zasady, że nie można ich używać razem. Dobrze jest zachować czystą i spójną bazę kodu, ale czasami można coś łatwiej zrobić za pomocą formularzy opartych na szablonach, a czasami jest odwrotnie. Tak więc, jeśli nie masz nic przeciwko nieco większym rozmiarom pakietów, sugeruję użycie tego, co uważasz za bardziej odpowiednie dla każdego przypadku. Po prostu nie mieszaj ich w tym samym komponencie, ponieważ prawdopodobnie spowoduje to pewne zamieszanie.

Plunkery używane w tym poście

  • Formularze oparte na szablonach

  • Formularze reaktywne, walidatory szablonów

  • Formularze reaktywne, walidatory kodu

  • Formularze reaktywne, kreator formularzy

  • Formularze oparte na szablonach, zagnieżdżone w tym samym komponencie

  • Formularze reaktywne, zagnieżdżone w tym samym komponencie

  • Formy zagnieżdżone poprzez drzewo komponentów

Powiązane: Inteligentne sprawdzanie poprawności formularzy Node.js