Arbeiten mit Angular 4-Formularen: Verschachtelung und Eingabevalidierung

Veröffentlicht: 2022-03-11

Im Web waren einige der frühesten Benutzereingabeelemente Schaltflächen, Kontrollkästchen, Texteingaben und Optionsfelder. Bis heute werden diese Elemente in modernen Webanwendungen verwendet, obwohl der HTML-Standard weit von seiner frühen Definition entfernt ist und jetzt alle möglichen ausgefallenen Interaktionen zulässt.

Die Validierung von Benutzereingaben ist ein wesentlicher Bestandteil jeder robusten Webanwendung.

Formulare in Angular-Anwendungen können den Status aller Eingaben unter diesem Formular aggregieren und einen Gesamtstatus wie den Validierungsstatus des vollständigen Formulars bereitstellen. Dies kann sehr praktisch sein, um zu entscheiden, ob die Benutzereingabe akzeptiert oder abgelehnt wird, ohne jede Eingabe einzeln zu prüfen.

Eingabevalidierung für Angular 4-Formulare

In diesem Artikel erfahren Sie, wie Sie in Ihrer Angular-Anwendung problemlos mit Formularen arbeiten und Formularvalidierungen durchführen können.

In Angular 4 stehen zwei verschiedene Arten von Formularen zur Verfügung, mit denen Sie arbeiten können: vorlagengesteuerte und reaktive Formulare. Wir werden jeden Formulartyp anhand desselben Beispiels durchgehen, um zu sehen, wie dieselben Dinge auf unterschiedliche Weise implementiert werden können. Später in diesem Artikel werden wir einen neuartigen Ansatz zum Einrichten und Arbeiten mit verschachtelten Formularen betrachten.

Eckige 4 Formen

In Angular 4 werden die folgenden vier Status häufig von Formularen verwendet:

  • gültig – Zustand der Gültigkeit aller Formularkontrollen, wahr, wenn alle Kontrollen gültig sind

  • ungültig – Umkehrung von valid ; true, wenn ein Steuerelement ungültig ist

  • makellos – gibt einen Status über die „Sauberkeit“ der Form an; true, wenn kein Steuerelement geändert wurde

  • schmutzig – Gegenteil von pristine ; true, wenn ein Steuerelement geändert wurde

Schauen wir uns ein einfaches Beispiel für ein Formular an:

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

Die Spezifikation für dieses Beispiel lautet wie folgt:

  • name - ist erforderlich und einzigartig unter allen registrierten Benutzern

  • Geburtsjahr – sollte eine gültige Zahl sein und der Benutzer muss mindestens 18 und weniger als 85 Jahre alt sein

  • Land - ist obligatorisch, und nur um die Dinge etwas komplizierter zu machen, brauchen wir eine Bestätigung, dass, wenn das Land Frankreich ist, die Stadt Paris sein muss (sagen wir, dass unser Service nur in Paris angeboten wird).

  • phoneNumber – jede Telefonnummer muss einem bestimmten Muster folgen, es muss mindestens eine Telefonnummer vorhanden sein, und der Benutzer darf eine neue Telefonnummer hinzufügen oder eine vorhandene Telefonnummer entfernen.

  • Die Schaltfläche „Registrieren“ wird nur aktiviert, wenn alle Eingaben gültig sind, und sendet das Formular nach dem Klicken ab.

  • „Auf Konsole drucken“ druckt einfach den Wert aller Eingaben auf die Konsole, wenn darauf geklickt wird.

Das ultimative Ziel ist die vollständige Umsetzung der definierten Spezifikation.

Vorlagengesteuerte Formulare

Vorlagengesteuerte Formulare sind den Formularen in AngularJS (oder Angular 1, wie manche es nennen) sehr ähnlich. Jemand, der mit Formularen in AngularJS gearbeitet hat, wird mit diesem Ansatz zum Arbeiten mit Formularen sehr vertraut sein.

Mit der Einführung von Modulen in Angular 4 wird erzwungen, dass sich jeder spezifische Formulartyp in einem separaten Modul befindet, und wir müssen explizit definieren, welchen Typ wir verwenden werden, indem wir das richtige Modul importieren. Dieses Modul für die vorlagengesteuerten Formulare ist FormsModule. Davon abgesehen können Sie die vorlagengesteuerten Formulare wie folgt aktivieren:

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

Wie in diesem Code-Snippet dargestellt, müssen wir zuerst das Browsermodul importieren, da es „Dienste bereitstellt, die zum Starten und Ausführen einer Browser-App unerlässlich sind“. (aus den Angular 4-Dokumenten). Dann importieren wir das erforderliche FormsModule, um die vorlagengesteuerten Formulare zu aktivieren. Und zuletzt ist die Deklaration der Root-Komponente, AppComponent, wo wir in den nächsten Schritten das Formular implementieren werden.

Denken Sie daran, dass Sie in diesem Beispiel und den folgenden Beispielen sicherstellen müssen, dass die App mithilfe der Methode „ platformBrowserDynamic “ ordnungsgemäß gebootet wird.

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

Wir können davon ausgehen, dass unsere AppComponent (app.component.ts) ungefähr so ​​aussieht:

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

Wo sich die Vorlage dieser Komponente in app.component.tpl.html befindet und wir die ursprüngliche Vorlage in diese Datei kopieren können.

Beachten Sie, dass jedes Eingabeelement das name haben muss, um innerhalb des Formulars richtig identifiziert zu werden. Obwohl dies wie ein einfaches HTML-Formular aussieht, haben wir bereits ein von Angular 4 unterstütztes Formular definiert (vielleicht sehen Sie es noch nicht). Wenn das FormsModule importiert wird, erkennt Angular 4 automatisch ein form -HTML-Element und hängt die NgForm-Komponente an dieses Element an (durch den selector der NgForm-Komponente). In unserem Beispiel ist das der Fall. Obwohl dieses Angular 4-Formular deklariert ist, kennt es zu diesem Zeitpunkt keine von Angular 4 unterstützten Eingaben. Angular 4 ist nicht so invasiv, um jedes input HTML-Element beim nächsten form zu registrieren.

Der Schlüssel, der es ermöglicht, dass ein Eingabeelement als Angular 4-Element erkannt und in der NgForm-Komponente registriert wird, ist die NgModel-Direktive. Wir können also die Vorlage app.component.tpl.html wie folgt erweitern:

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

Durch Hinzufügen der NgModel-Direktive werden alle Eingaben in der NgForm-Komponente registriert. Damit haben wir ein voll funktionsfähiges Angular 4-Formular definiert und so weit, so gut, aber wir haben immer noch keine Möglichkeit, auf die NgForm-Komponente und die von ihr angebotenen Funktionalitäten zuzugreifen. Die beiden Hauptfunktionalitäten von NgForm sind:

  • Abrufen der Werte aller registrierten Eingabesteuerelemente

  • Abrufen des Gesamtzustands aller Steuerelemente

Um das NgForm verfügbar zu machen, können wir dem <form>-Element Folgendes hinzufügen:

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

Dies ist dank der exportAs Eigenschaft des Component -Dekorators möglich.

Sobald dies erledigt ist, können wir auf die Werte aller Eingabesteuerelemente zugreifen und die Vorlage erweitern auf:

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

Mit myForm.value wir auf JSON-Daten zu, die die Werte aller registrierten Eingaben enthalten, und mit {{myForm.value | json}} {{myForm.value | json}} , drucken wir den JSON mit den Werten.

Was ist, wenn wir eine Untergruppe von Eingaben aus einem bestimmten Kontext in einen Container packen und ein separates Objekt in den JSON-Werten haben möchten, z. B. Standort, der Land und Stadt oder die Telefonnummern enthält? Machen Sie sich keinen Stress – vorlagengesteuerte Formulare in Angular 4 decken das ebenfalls ab. Der Weg, dies zu erreichen, ist die Verwendung der ngModelGroup Direktive.

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

Was uns jetzt fehlt, ist eine Möglichkeit, mehrere Telefonnummern hinzuzufügen. Der beste Weg, dies zu tun, wäre die Verwendung eines Arrays als beste Darstellung eines iterierbaren Containers mit mehreren Objekten gewesen, aber zum Zeitpunkt des Schreibens dieses Artikels ist diese Funktion für die vorlagengesteuerten Formulare nicht implementiert. Wir müssen also eine Problemumgehung anwenden, damit dies funktioniert. Der Abschnitt Telefonnummern muss wie folgt aktualisiert werden:

 <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() wird verwendet, um das Formular touched zu machen, damit wir die Fehler in diesem Moment anzeigen können. Die Schaltflächen aktivieren diese Eigenschaft nicht, wenn sie angeklickt werden, sondern nur die Eingänge. Um die nächsten Beispiele klarer zu machen, füge ich diese Zeile nicht zum Click-Handler für add() und remove() hinzu. Stellen Sie sich vor, es wäre da. (Es ist in den Plunkers vorhanden.)

Wir müssen auch AppComponent aktualisieren, damit es den folgenden Code enthält:

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

Wir müssen eine eindeutige ID für jede neu hinzugefügte Telefonnummer speichern und in *ngFor die Telefonnummern-Steuerelemente anhand ihrer ID verfolgen (ich gebe zu, es ist nicht sehr schön, aber bis das Angular 4-Team diese Funktion implementiert, fürchte ich , es ist das Beste, was wir tun können)

Okay, was haben wir bisher, wir haben das von Angular 4 unterstützte Formular mit Eingaben hinzugefügt, eine bestimmte Gruppierung der Eingaben (Standort und Telefonnummern) hinzugefügt und das Formular innerhalb der Vorlage bereitgestellt. Was aber, wenn wir in einer Methode in der Komponente auf das NgForm-Objekt zugreifen möchten? Wir werden uns zwei Möglichkeiten ansehen, dies zu tun.

Für den ersten Weg kann das myForm , im aktuellen Beispiel als myForm bezeichnet, als Argument an die Funktion übergeben werden, die als Handler für das onSubmit-Ereignis des Formulars dient. Für eine bessere Integration wird das wrapped -Ereignis von einem Angular 4, NgForm-spezifischen Ereignis namens ngSubmit , und dies ist der richtige Weg, wenn wir beim Senden eine Aktion ausführen möchten. Also sieht das Beispiel jetzt so aus:

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

Wir müssen ein entsprechendes Methodenregister haben, das in der register implementiert ist. Etwas wie:

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

Auf diese Weise haben wir durch die Nutzung des onSubmit-Ereignisses nur dann Zugriff auf die NgForm-Komponente, wenn das Senden ausgeführt wird.

Die zweite Möglichkeit besteht darin, eine Ansichtsabfrage zu verwenden, indem der @ViewChild-Dekorator zu einer Eigenschaft der Komponente hinzugefügt wird.

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

Mit diesem Ansatz haben wir Zugriff auf das Formular, unabhängig davon, ob das onSubmit-Ereignis ausgelöst wurde oder nicht.

Toll! Jetzt haben wir ein voll funktionsfähiges Angular 4-Formular mit Zugriff auf das Formular in der Komponente. Aber merkst du, dass etwas fehlt? Was ist, wenn der Benutzer etwas wie „das-ist-kein-Jahr“ in die Eingabe „Jahre“ eingibt? Ja, Sie haben es verstanden, uns fehlt die Validierung der Eingaben, und wir werden das im folgenden Abschnitt behandeln.

Validierung

Die Validierung ist wirklich wichtig für jede Anwendung. Wir möchten die Benutzereingaben immer validieren (wir können dem Benutzer nicht vertrauen), um das Senden/Speichern ungültiger Daten zu verhindern, und wir müssen eine aussagekräftige Meldung über den Fehler anzeigen, um den Benutzer ordnungsgemäß zur Eingabe gültiger Daten zu führen.

Damit einige Validierungsregeln für einige Eingaben erzwungen werden, muss der richtige Validierer dieser Eingabe zugeordnet werden. Angular 4 bietet bereits eine Reihe gängiger Validatoren wie: required , maxLength , minLength

Wie können wir also einen Validator mit einer Eingabe verknüpfen? Nun, ziemlich einfach; Fügen Sie einfach die Validator-Direktive zum Steuerelement hinzu:

 <input name="name" ngModel required/>

Dieses Beispiel macht die Eingabe von „Name“ obligatorisch. Fügen wir allen Eingaben in unserem Beispiel einige Validierungen hinzu.

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

Hinweis: novalidate wird verwendet, um die native Formularvalidierung des Browsers zu deaktivieren.

Wir haben den „Namen“ zum Pflichtfeld gemacht, das Feld „Jahre“ ist Pflichtfeld und darf nur aus Zahlen bestehen, die Ländereingabe ist Pflichtfeld und auch die Telefonnummer ist Pflichtfeld. Außerdem drucken wir den Status der Gültigkeit des Formulars mit {{myForm.valid}} .

Eine Verbesserung dieses Beispiels wäre, auch zu zeigen, was mit der Benutzereingabe nicht stimmt (nicht nur den Gesamtzustand anzuzeigen). Bevor wir mit dem Hinzufügen zusätzlicher Validierung fortfahren, möchte ich eine Hilfskomponente implementieren, die es uns ermöglicht, alle Fehler für ein bereitgestelltes Steuerelement zu drucken.

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

Die Liste mit Fehlern wird nur angezeigt, wenn einige Fehler vorhanden sind und die Eingabe berührt oder verschmutzt ist.

Die Meldung für jeden Fehler wird in einer Karte vordefinierter Meldungen errorMessages (ich habe alle Meldungen im Voraus hinzugefügt).

Diese Komponente kann wie folgt verwendet werden:

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

Wir müssen das NgModel für jede Eingabe verfügbar machen und es an die Komponente übergeben, die alle Fehler rendert. Sie können feststellen, dass wir in diesem Beispiel ein Muster verwendet haben, um zu prüfen, ob die Daten eine Zahl sind; Was ist, wenn der Benutzer „0000“ eingibt? Dies wäre eine ungültige Eingabe. Außerdem fehlen uns die Validatoren für einen eindeutigen Namen, die seltsame Beschränkung des Landes (wenn Land='Frankreich', dann muss die Stadt 'Paris' sein), Muster für eine korrekte Telefonnummer und die Überprüfung, dass mindestens eine Telefonnummer vorhanden ist existiert. Dies ist der richtige Zeitpunkt, um einen Blick auf benutzerdefinierte Validatoren zu werfen.

Angular 4 bietet eine Schnittstelle, die jeder benutzerdefinierte Validator implementieren muss, die Validator-Schnittstelle (was für eine Überraschung!). Die Validator-Oberfläche sieht im Wesentlichen so aus:

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

Wobei jede konkrete Implementierung die „validate“-Methode implementieren MUSS. Diese validate ist wirklich interessant, was als Eingabe empfangen werden kann und was als Ausgabe zurückgegeben werden soll. Die Eingabe ist ein AbstractControl, was bedeutet, dass das Argument ein beliebiger Typ sein kann, der AbstractControl erweitert (FormGroup, FormControl und FormArray). Die Ausgabe der validate -Methode sollte null oder undefined (keine Ausgabe) sein, wenn die Benutzereingabe gültig ist, oder ein ValidationErrors -Objekt zurückgeben, wenn die Benutzereingabe ungültig ist. Mit diesem Wissen werden wir nun einen benutzerdefinierten birthYear Validator implementieren.

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

Hier gibt es einiges zu erklären. Zunächst fällt Ihnen vielleicht auf, dass wir die Validator-Schnittstelle implementiert haben. Die validate prüft, ob der Benutzer zum eingegebenen Geburtsjahr zwischen 18 und 85 Jahre alt ist. Wenn die Eingabe gültig ist, wird null zurückgegeben, oder es wird ein Objekt zurückgegeben, das die Validierungsnachricht enthält. Und der letzte und wichtigste Teil ist die Deklaration dieser Direktive als Validator. Dies geschieht im „providers“-Parameter des @Directive-Decorators. Dieser Validator wird als ein Wert des Multiproviders NG_VALIDATORS bereitgestellt. Vergessen Sie auch nicht, diese Direktive im NgModule zu deklarieren. Und jetzt können wir diesen Validator wie folgt verwenden:

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

Ja, so einfach!

Für die Telefonnummer können wir das Format der Telefonnummer wie folgt validieren:

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

Jetzt kommen die beiden Validierungen, für das Land und die Anzahl der Telefonnummern. Fällt ihnen etwas Gemeinsames auf? Beide erfordern mehr als eine Kontrolle, um eine ordnungsgemäße Validierung durchzuführen. Erinnerst du dich an die Validator-Oberfläche und was wir darüber gesagt haben? Das Argument der Methode validate ist AbstractControl, das eine Benutzereingabe oder das Formular selbst sein kann. Dies schafft die Möglichkeit, einen Validator zu implementieren, der mehrere Kontrollen verwendet, um den konkreten Validierungsstatus zu ermitteln.

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

Wir haben einen neuen Validator implementiert, den Land-Stadt-Validator. Sie können feststellen, dass die validate-Methode jetzt als Argument eine FormGroup erhält und wir aus dieser FormGroup die für die Validierung erforderlichen Eingaben abrufen können. Der Rest ist dem Single-Input-Validator sehr ähnlich.

Der Validator für die Anzahl der Telefonnummern sieht folgendermaßen aus:

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

Wir können sie wie folgt verwenden:

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

Dasselbe wie die Eingabevalidatoren, oder? Gerade jetzt auf das Formular angewendet.

Erinnern Sie sich an die ShowErrors-Komponente? Wir haben es so implementiert, dass es mit einer AbstractControlDirective funktioniert, was bedeutet, dass wir es wiederverwenden könnten, um auch alle Fehler anzuzeigen, die direkt mit diesem Formular verbunden sind. Beachten Sie, dass zu diesem Zeitpunkt die einzigen direkt mit dem Formular verknüpften Validierungsregeln Country-city und Telephone numbers (die anderen Validierer sind mit den spezifischen Formularsteuerelementen verknüpft). Um alle Formularfehler auszudrucken, gehen Sie einfach wie folgt vor:

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

Als letztes bleibt noch die Validierung für einen eindeutigen Namen. Dies ist ein bisschen anders; Um zu überprüfen, ob der Name eindeutig ist, ist höchstwahrscheinlich ein Aufruf an das Backend erforderlich, um alle vorhandenen Namen zu überprüfen. Dies wird als asynchroner Vorgang klassifiziert. Zu diesem Zweck können wir die vorherige Technik für benutzerdefinierte Validatoren wiederverwenden, lassen Sie einfach die validate ein Objekt zurückgeben, das irgendwann in der Zukunft aufgelöst wird (Promise oder Observable). In unserem Fall verwenden wir ein Versprechen:

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

Wir warten 1 Sekunde und geben dann ein Ergebnis zurück. Wenn das Promise mit null aufgelöst wird, bedeutet das ähnlich wie bei den Sync-Validatoren, dass die Validierung bestanden wurde; Wenn das Versprechen mit etwas anderem aufgelöst wird, ist die Validierung fehlgeschlagen. Beachten Sie auch, dass dieser Validator jetzt bei einem anderen Multi-Provider registriert ist, dem NG_ASYNC_VALIDATORS . Eine nützliche Eigenschaft der Formulare in Bezug auf die asynchronen Validatoren ist die pending -Eigenschaft. Es kann wie folgt verwendet werden:

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

Die Schaltfläche wird deaktiviert, bis die asynchronen Validatoren aufgelöst sind.

Hier ist ein Plunker, der die vollständige AppComponent, die ShowErrors-Komponente und alle Validatoren enthält.

Mit diesen Beispielen haben wir die meisten Fälle für die Arbeit mit vorlagengesteuerten Formularen abgedeckt. Wir haben gezeigt, dass vorlagengesteuerte Formulare den Formularen in AngularJS sehr ähnlich sind (es wird für AngularJS-Entwickler wirklich einfach sein, zu migrieren). Bei diesem Formulartyp ist es recht einfach, Angular-4-Formulare mit minimalem Programmieraufwand einzubinden, hauptsächlich durch Manipulationen im HTML-Template.

Reaktive Formen

Die reaktiven Formulare waren auch als „modellgesteuerte“ Formulare bekannt, aber ich nenne sie gerne „programmatische“ Formulare, und bald werden Sie sehen, warum. Die reaktiven Formulare sind ein neuer Ansatz zur Unterstützung von Angular 4-Formularen, sodass AngularJS-Entwickler im Gegensatz zu den vorlagengesteuerten Formularen nicht mit diesem Typ vertraut sein werden.

Wir können jetzt beginnen, erinnern Sie sich, dass die vorlagengesteuerten Formulare ein spezielles Modul hatten? Nun, die reaktiven Formulare haben auch ihr eigenes Modul namens ReactiveFormsModule und müssen importiert werden, um diese Art von Formularen zu aktivieren.

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

Vergessen Sie auch nicht, die Anwendung zu booten.

Wir können mit der gleichen AppComponent und Vorlage wie im vorherigen Abschnitt beginnen.

Wenn das FormsModule an diesem Punkt nicht importiert wird (und stellen Sie bitte sicher, dass dies nicht der Fall ist), haben wir nur ein reguläres HTML-Formularelement mit ein paar Formularsteuerelementen, hier keine Angular-Magie.

Wir kommen zu dem Punkt, an dem Sie feststellen werden, warum ich diesen Ansatz gerne „programmatisch“ nenne. Um Angular 4-Formulare zu aktivieren, müssen wir das FormGroup-Objekt manuell deklarieren und es mit Steuerelementen wie diesem füllen:

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

Die Methoden printForm und register sind die gleichen wie in den vorherigen Beispielen und werden in den nächsten Schritten verwendet. Die hier verwendeten Schlüsseltypen sind FormGroup, FormControl und FormArray. Diese drei Typen sind alles, was wir brauchen, um eine gültige FormGroup zu erstellen. Die FormGroup ist einfach; Es ist ein einfacher Container mit Steuerelementen. Das FormControl ist auch einfach; es ist eine beliebige Steuerung (z. B. Eingabe). Und schließlich ist das FormArray das Puzzleteil, das uns beim vorlagengesteuerten Ansatz gefehlt hat. Das FormArray ermöglicht das Verwalten einer Gruppe von Steuerelementen, ohne einen konkreten Schlüssel für jedes Steuerelement anzugeben, im Grunde ein Array von Steuerelementen (scheint das perfekte Ding für die Telefonnummern zu sein, oder?).

Wenn Sie einen dieser drei Typen konstruieren, denken Sie an diese 3er-Regel. Der Konstruktor für jeden Typ erhält drei Argumente – value , Validator oder eine Liste von Validatoren und einen asynchronen Validator oder eine Liste von asynchronen Validatoren, die im Code definiert sind:

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

Bei FormGroup ist der value ein Objekt, bei dem jeder Schlüssel den Namen eines Steuerelements darstellt und der Wert das Steuerelement selbst ist.

Bei FormArray ist der value ein Array von Steuerelementen.

Für FormControl ist der value der Anfangswert oder der Anfangszustand (Objekt, das einen value und eine disabled Eigenschaft enthält) des Steuerelements.

Wir haben das FormGroup-Objekt erstellt, aber die Vorlage kennt dieses Objekt noch nicht. Die Verknüpfung zwischen der FormGroup in der Komponente und der Vorlage erfolgt mit vier Direktiven: formGroup , formControlName , formGroupName und formArrayName , die wie folgt verwendet werden:

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

Jetzt, da wir das FormArray haben, können Sie sehen, dass wir diese Struktur zum Rendern aller Telefonnummern verwenden können.

Und nun zum Hinzufügen der Unterstützung für das Hinzufügen und Entfernen von Telefonnummern (in der Komponente):

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

Jetzt haben wir ein voll funktionsfähiges reaktives Angular 4-Formular. Beachten Sie den Unterschied zu den vorlagengesteuerten Formularen, bei denen die FormGroup „in der Vorlage erstellt“ (durch Scannen der Vorlagenstruktur) und an die Komponente übergeben wurde, bei den reaktiven Formularen ist es umgekehrt, die vollständige FormGroup wird in der erstellt Komponente, dann „an die Vorlage übergeben“ und mit den entsprechenden Steuerelementen verknüpft. Aber wir haben wieder das gleiche Problem mit der Validierung, ein Problem, das im nächsten Abschnitt behoben wird.

Validierung

In Bezug auf die Validierung sind die reaktiven Formulare viel flexibler als die vorlagengesteuerten Formulare. Ohne zusätzliche Änderungen können wir dieselben Validatoren wiederverwenden, die zuvor implementiert wurden (für die vorlagengesteuerte). Durch Hinzufügen der Validator-Direktiven können wir also dieselbe Validierung aktivieren:

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

Denken Sie daran, dass wir jetzt nicht die NgModel-Direktive haben, um sie an die ShowErrors-Komponente zu übergeben, aber die vollständige FormGroup ist bereits erstellt und wir können das richtige AbstractControl zum Abrufen der Fehler übergeben.

Hier ist ein voll funktionsfähiger Plunker mit dieser Art der Validierung für reaktive Formulare.

Aber es würde keinen Spaß machen, wenn wir einfach die Validatoren wiederverwenden würden, oder? Wir werden uns ansehen, wie die Prüfer beim Erstellen der Formulargruppe angegeben werden.

Erinnern Sie sich an die „3s-Regel“, die wir über den Konstruktor für FormGroup, FormControl und FormArray erwähnt haben? Ja, wir haben gesagt, dass der Konstruktor Validierungsfunktionen empfangen kann. Versuchen wir also diesen Ansatz.

Zuerst müssen wir die validate aller Validatoren in eine Klasse extrahieren, die sie als statische Methoden verfügbar macht:

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

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

Wenn Sie das wissen, denken Sie darüber nach, was wir tun, wenn wir den Formularstatus aggregieren. Richtig, Daten werden im Komponentenbaum nach oben geschoben. Auch beim Arbeiten mit einstufigen Formularen ist die Integration der Formularsteuerelemente ( ngModel ) und des Formulars selbst nicht so schön. Sie lösen einen zusätzlichen Änderungserkennungszyklus aus, wenn der Wert eines Steuerelements registriert oder aktualisiert wird (dies geschieht mit einem aufgelösten Versprechen, aber halten Sie es geheim). Warum wird eine zusätzliche Runde benötigt? Nun, aus dem gleichen Grund fließen Daten nach oben, vom Steuerelement zum Formular. Aber manchmal ist das Verschachteln von Formularen über mehrere Komponenten hinweg eine erforderliche Funktion, und wir müssen uns eine Lösung überlegen, um diese Anforderung zu unterstützen.

Mit dem, was wir bisher wissen, ist die erste Idee, die uns in den Sinn kommt, reaktive Formulare zu verwenden, den vollständigen Formularbaum in einer Stammkomponente zu erstellen und dann die untergeordneten Formulare als Eingaben an die untergeordneten Komponenten zu übergeben. Auf diese Weise haben Sie die übergeordnete Komponente eng mit den untergeordneten Komponenten gekoppelt und die Geschäftslogik der Stammkomponente mit der Handhabung der Erstellung aller untergeordneten Formulare überladen. Komm schon, wir sind Profis, ich bin sicher, wir können einen Weg finden, völlig isolierte Komponenten mit Formularen zu erstellen und dem Formular eine Möglichkeit zu bieten, den Zustand einfach an den Elternteil weiterzugeben.

Abgesehen davon ist hier eine Direktive, die das Verschachteln von Angular 4-Formularen ermöglicht (implementiert, weil sie für ein Projekt benötigt wurde):

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

Das Beispiel im folgenden GIF zeigt eine main , die form-1 enthält, und innerhalb dieses Formulars gibt es eine weitere verschachtelte Komponente, component-2 . component-2 enthält form-2 , das form-2.1 , form-2.2 , und eine Komponente ( component-3 ) mit einem Baum eines reaktiven Formulars enthält, und eine Komponente ( component-4 ), die ein Formular enthält, das verschachtelt ist ist von allen anderen Formen isoliert. Ziemlich chaotisch, ich weiß, aber ich wollte ein ziemlich komplexes Szenario erstellen, um die Funktionalität dieser Direktive zu zeigen.

Ein komplexer Fall der Validierung von Angular-Formularen mit mehreren Komponenten

Das Beispiel ist in diesem Plunker implementiert.

Die Funktionen, die es bietet, sind:

  • Aktiviert das Verschachteln durch Hinzufügen der Direktive nestableForm zu Elementen: form, ngForm, [ngForm], [formGroup]

  • Funktioniert mit vorlagengesteuerten und reaktiven Formularen

  • Ermöglicht das Erstellen einer Formularstruktur, die mehrere Komponenten umfasst

  • Isoliert einen Teilbaum von Formularen mit rootNestableForm=”true” (es wird nicht beim übergeordneten nestableForm registriert)

Diese Direktive ermöglicht es einem Formular in einer untergeordneten Komponente, sich bei der ersten übergeordneten nestableForm zu registrieren, unabhängig davon, ob das übergeordnete Formular in derselben Komponente deklariert ist oder nicht. Wir gehen auf die Details der Umsetzung ein.

Schauen wir uns zunächst den Konstruktor an. Das erste Argument ist:

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

Dadurch wird das erste NestableFormDirective-Elternelement gesucht. @SkipSelf, um nicht mit sich selbst übereinzustimmen, und @Optional, weil es im Fall des Stammformulars möglicherweise kein übergeordnetes Element findet. Jetzt haben wir einen Verweis auf das verschachtelbare übergeordnete Formular.

Das zweite Argument ist:

 private injector: Injector

Der Injektor wird verwendet, um den aktuellen FormGroup- provider (Vorlage oder reaktiv) abzurufen.

Und das letzte Argument ist:

 @Attribute('rootNestableForm') private isRoot

um den Wert zu erhalten, der bestimmt, ob dieses Formular vom Formularbaum isoliert ist.

Als nächstes wird bei ngInit als verschobene Aktion (erinnern Sie sich an den umgekehrten Datenfluss?) die aktuelle FormGroup aufgelöst, ein neues FormArray-Steuerelement namens CHILD_FORMS wird in dieser FormGroup registriert (wo untergeordnete Formulare registriert werden) und als letzte Aktion die Die aktuelle FormGroup ist als untergeordnetes Element des verschachtelbaren übergeordneten Formulars registriert.

Die Aktion ngOnDestroy wird ausgeführt, wenn das Formular zerstört wird. Beim Zerstören, wiederum als aufgeschobene Aktion, wird das aktuelle Formular aus dem übergeordneten Formular entfernt (Abmeldung).

Die Anweisung für verschachtelbare Formulare kann weiter an spezielle Anforderungen angepasst werden – vielleicht die Unterstützung für reaktive Formulare entfernen, jedes untergeordnete Formular unter einem bestimmten Namen registrieren (nicht in einem Array CHILD_FORMS) und so weiter. Diese Implementierung der nestableForm-Direktive erfüllte die Anforderungen des Projekts und wird hier als solche präsentiert. Es behandelt einige grundlegende Fälle wie das Hinzufügen eines neuen Formulars oder das dynamische Entfernen eines vorhandenen Formulars (*ngIf) und das Weitergeben des Status des Formulars an das übergeordnete Formular. Dies läuft im Grunde auf Vorgänge hinaus, die innerhalb eines Änderungserkennungszyklus (mit oder ohne Verschiebung) gelöst werden können.

Wenn Sie ein fortgeschritteneres Szenario wünschen, wie z. B. das Hinzufügen einer bedingten Validierung zu einer Eingabe (z. B. [required]=“someCondition“), die 2 Änderungserkennungsrunden erfordern würde, funktioniert dies aufgrund der „one-detection-cycle-resolution“-Regel nicht von Angular 4 auferlegt.

Wie auch immer, wenn Sie vorhaben, diese Direktive zu verwenden oder eine andere Lösung zu implementieren, seien Sie sehr vorsichtig in Bezug auf die Dinge, die in Bezug auf die Änderungserkennung erwähnt wurden. An dieser Stelle wird Angular 4 so implementiert. Es könnte sich in Zukunft ändern – wir können es nicht wissen. Das aktuelle Setup und die erzwungene Einschränkung in Angular 4, die in diesem Artikel erwähnt wurde, kann ein Nachteil oder ein Vorteil sein. Es bleibt abzuwarten.

Formulare leicht gemacht mit Angular 4

Wie Sie sehen können, hat das Angular-Team bei der Bereitstellung vieler Funktionalitäten im Zusammenhang mit Formularen wirklich gute Arbeit geleistet. Ich hoffe, dass dieser Beitrag als vollständiger Leitfaden für die Arbeit mit den verschiedenen Arten von Formularen in Angular 4 dient und auch einen Einblick in einige fortgeschrittenere Konzepte wie das Verschachteln von Formularen und den Prozess der Änderungserkennung gibt.

Trotz all der unterschiedlichen Beiträge zu Angular-4-Formularen (oder zu anderen Angular-4-Themen) ist meiner Meinung nach der beste Ausgangspunkt die offizielle Angular-4-Dokumentation. Außerdem haben die Angular-Jungs eine schöne Dokumentation in ihrem Code. Oft habe ich eine Lösung gefunden, indem ich mir nur den Quellcode und die Dokumentation dort angeschaut habe, kein Googeln oder so. Was die Verschachtelung von Formularen betrifft, die im letzten Abschnitt besprochen wurde, glaube ich, dass jeder AngularJS-Entwickler, der anfängt, Angular 4 zu lernen, irgendwann auf dieses Problem stoßen wird, was mich irgendwie dazu inspiriert hat, diesen Beitrag zu schreiben.

Wie wir auch gesehen haben, gibt es zwei Arten von Formularen, und es gibt keine strikte Regel, dass Sie sie nicht zusammen verwenden können. Es ist schön, die Codebasis sauber und konsistent zu halten, aber manchmal kann etwas mit vorlagengesteuerten Formularen einfacher erledigt werden, und manchmal ist es umgekehrt. Wenn Sie also die etwas größeren Bündelgrößen nicht stören, schlage ich vor, von Fall zu Fall das zu verwenden, was Sie für angemessener halten. Mischen Sie sie nur nicht innerhalb derselben Komponente, da dies wahrscheinlich zu Verwirrung führen wird.

In diesem Beitrag verwendete Plunker

  • Vorlagengesteuerte Formulare

  • Reaktive Formulare, Vorlagenvalidatoren

  • Reaktive Formulare, Code-Validatoren

  • Reaktive Formulare, Formularersteller

  • Vorlagengesteuerte Formulare, verschachtelt in derselben Komponente

  • Reaktive Formulare, verschachtelt in derselben Komponente

  • Verschachtelte Formulare durch Komponentenbaum

Verwandt: Smart Node.js-Formularvalidierung