Dieser Blog richtet sich an die Leserschaft, die bereits mit Angular vertraut sind, jedoch ihr Wissen über Forms vertiefen wollen. Wer über weniger Erfahrung verfügt, benutzt besser die detailliertere Schrit um Schritt Beschreibung mit den zur Verfügung gestellten Anwendungsbeispielen, welche hier zu finden sind.
Views bzw. Forms übernehmen bei Web-Applikationen eine bedeutende Rolle. Sie verbinden Mensch und Maschine und sind dafür verantwortlich, dass deren Austausch von Informationen verständlich und fehlerfrei stattfindet. Die vom Benutzer eingegebenen Daten werden validiert und nur dann an die nächste Schicht übergeben, wenn sie fehlerfrei sind. Sind die Eingabedaten falsch, muss der Benutzer unmittelbar nach der Eingabe verständlich darüber informiert werden.
Typische Anforderungen an Forms sind zum Beispiel:
Strategie der Validierung
Eingabedaten sollen unmittelbar nach ihrer Eingabe validiert und allfällige Eingabefehler dem Benutzer mitgeteilt werden. Nicht bediente Pflichtfelder oder fehlerhafte Felder sollen dazu führen, dass ein Formular nicht versendet wird.
Der Button «Submit» kann durch den Benutzer erst dann betätigt werden, wenn die Daten gültig, vollständig und fehlerfrei sein.
Oft werden bei Web-Applikationen Validierungsmitteilungen erst dann angezeigt, wenn der Sendebutton betätigt wird. Das trifft meistens dann zu, wenn die Daten serverseitig geprüft werden. Diese Strategie ist weniger benutzerfreundlich und sollte deshalb nicht angewendet werden.
Wann der Validierungszeitpunkt stattfinden soll, kann über die ngFormOptions bestimmt werden. Mehr dazu in den untenstehenden Kapiteln.
Technologien
Angular verfolgt zur Erstellung von Formularen zwei Techniken. Model-Driven (reaktiver Ansatz) oder Template-Driven:
Template-Driven benötigt das Model nicht, die Definitionen werden deklarativ im HTML vorgenommen. Dabei kommt das Forms-API zum Tragen, welches im Hauptmodul mittels FormsModule wie folgt importiert und registriert wird.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { ReaktivFormComponent } from './view/reaktiv-form/reaktiv-form.component'; import { TemplateFormComponent } from './view/template-form/template-form.component'; import { UniqueEmailValidatorDirective } from './directiven/emailUnique.directive'; @NgModule({ declarations: [ AppComponent, ReaktivFormComponent, TemplateFormComponent, UniqueEmailValidatorDirective ], imports: [ BrowserModule, AppRoutingModule, FormsModule, ReactiveFormsModule, HttpClientModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Dadurch wird die direktive NgForm erzeugt und kann wie im Beispiel der lokalen Variable «cutomerForm» zugewiesen werden. (Achtung, diese Variable ist nur innerhalb des Templates gültig).
<div class="container"> <div class="row"> <div class="col-md-6 offset-md-3"> <h1>Template-Driven Form</h1> <form novalidate #customerForm="ngForm" [ngFormOptions]="{ updateOn: 'blur' }"> <div [hidden]="customerForm.submitted"> <div class="form-group"> <label for="email">Email:</label> <input id="email" name="email" class="form-control" appEmailValidator [(ngModel)]="Email" #email="ngModel"> <div *ngIf="email.invalid && (email.dirty || email.touched)" class="alert alert-danger"> <div *ngIf="email.errors?.EmailValidatorMsg"> {{email.errors.EmailValidatorMsg}} </div> </div> </div> . . .
Dies führt dazu, dass Properties bereitgestellt werden, die den Kontrollstatus und die Gültigkeit des Forms verfolgen. Folgende Properties und Methoden stehen zur Verfügung:
Properties Methode | Bedeutung |
submitted | Gibt zurück, ob eine Form-Übermittlung ausgelöst wurde |
errors | Gibt an, ob ein Form fehlerhaft ist. Tritt mindestens ein Fehler in einem Eingabefeld auf, hat die Variable den Wert «true» |
touched | Gibt an, ob ein Eingabefeld den Fokus erhielt oder ein Mouse-Event ausgelöst wurde. |
dirty | Wird ein Wert in einem Eingabefeld nach der Eingabe verändert, hat die Variable «dirty» den Wert true |
resetForm() | Alle Eingabefelder werden zurückgesetzt. |
controls | Gibt ein Instanz der FormControl aus der FormGroup zurück. |
Folgendes Beispiel zeigt, wie die Properties von ngForm angewendet werden:
<button type="submit" class="btn btn-default" [disabled]="customerForm.invalid"> Senden </button>
|
Der Button «Senden» wird disabled dargestellt, falls das Formular fehlerhafte Daten hat. In diesem konkreten Fall wird das HTML-Property «disabled» an die Variable «customerForm.invalid» gebunden. Eine Änderung dieses Variablenwertes wird somit gleich an dieses HTML-Property übertragen. Sobald das Formular valide ist, wird auch der Button enabled dargestellt. Dies geschieht über das One-Way-Binding. |
Eine weitere Möglichkeit zur Validierung bieten Direktiven. Folgende wird hier für die Prüfung bereits registrierter Emailadressen verwendet
import { Directive, forwardRef, Injectable } from '@angular/core'; import { of, Observable } from 'rxjs'; import { EmailService } from '../service/email.service'; import { AsyncValidator, AbstractControl, NG_ASYNC_VALIDATORS, ValidationErrors } from '@angular/forms'; import { catchError, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class UniqueEmailValidator implements AsyncValidator { constructor(private emailService: EmailService) { } validate( ctrl: AbstractControl ): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> { console.log('in Validation'); return this.emailService.isEmailAlreadyRegistered(ctrl.value).pipe( map(isTaken => (isTaken ? { uniqueEmail: true } : null)), catchError(() => null) ); } } @Directive({ selector: '[appUniqueEmail]', providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => UniqueEmailValidator), multi: true } ] }) export class UniqueEmailValidatorDirective { constructor(private validator: UniqueEmailValidator) { } validate(control: AbstractControl) { this.validator.validate(control); } }
und wird im Template wie folgt integriert.
<div class="form-group"> <label for="email">Email</label> <input id="email" class="form-control" name="email" #email="ngModel" [(ngModel)]="customer.email" [ngModelOptions]="{ updateOn: 'blur' }" appUniqueEmail> <div *ngIf="email.pending">Validating...</div> <div *ngIf="email.invalid" class="alert alert-danger"> email is already taken. </div> </div>
In dieser Direktive wird ein Http-Service verwendet, damit in einem externen Server die Prüfung durchgeführt werden kann, ob eine Emailadresse für eine Registrierung bereits schon verwendet wurde.
import { Injectable } from '@angular/core'; import { Observable, of, throwError } from 'rxjs'; import { delay, catchError, retry, map } from 'rxjs/operators'; import { HttpClient, HttpHeaders, HttpErrorResponse, HttpParams } from '@angular/common/http'; import { tap } from 'rxjs/internal/operators'; @Injectable({ providedIn: 'root' }) export class EmailService { constructor(private http: HttpClient) { } isEmailAlreadyRegistered(email: string): Observable<boolean> { const options = email ? { params: new HttpParams().set('email', email) } : { headers: new HttpHeaders().set('Content-Type', 'application/json') }; return this.http.get('http://localhost:60490/RegisteredEmail', options).pipe(map(res => { console.log(res); return res == "1" ? true : false })); } }
Da die Anbindung über einen externen Server erfolgt, erhalten wir das Prüfergebnis asynchron und verwenden deshalb in der Directive das NG_ASYNC_VALIDATORS-Token. Pipe bietet hier die Möglichkeiten, den Operator map auf die Werte des Streams anzuwenden, um diesen in einer veränderten Form dem Stream wieder zur Verfügung zu stellen. Konkret, mit dem Operator map wird der Boolean aus dem Stream in ein ValidationErrors-Objekt transformiert und an den Stream weiter geleitet.
Wann übrigens eine Validierung stattfinden soll, kann im Template mittels ngModelOptions im Property UpdateOn bestimmt werden. Folgende Möglichkeiten stehen zur Verfügung:
Property | Bedeutung |
Blur | Validierung erfolgt, wenn der Benutzer das Eingabefeld verlässt. |
update | Bei jeder Veränderung des Wertes im Eingabefeld, oder beim Eintippen eines Wertes nach jedem Buchstaben. |
submit | Zum Zeitpunkt, wenn das Formular abgeschickt wird. |
Übrigens lohnt es sich, den Decorator einer Directive genauer unter die Lupe zu nehmen. Hier bestehen Möglichkeiten, das Verhalten und weitere Eigenschaften zu definieren. Z.B. auf welche Formulare eine Directive angewendet werden soll etc.
Bei der Anwendung von Template-Driven entfällt viel Implementationsaufwand und oft ist diese Methode hinreichend, um übliche Anforderungen zu erfüllen. Wie auch im konkreten Fall, wo ein Http-Service verwendet wird. Ein durchaus grosser Vorteil dieser Methode ist, dass Validierungen angepasst werden können, ohne dabei eine Zeile Typescript hinzuzufügen. Dies kann unkompliziert in der Form erweitert werden. Jedoch übersteigt die Projektgrösse ein bestimmtes Mass, führt diese Methode zu einer schlechteren Übersicht. Zudem verkompliziert sich bei kombinierten Anwendungen die Integration von synchronen und asynchronen Anwendungsblöcken.
Die Funtionsweise
Bei Reaktiven Template steht das Model im Vordergrund. Auch hier kann im Template eine lokale Variable verwendet werden, welche ihre Daten aus dem Model bezieht. Dies geschieht nicht wie bei Template-Driven automatisch, sondern wird über den Konstruktor in das Model injected. Um diese Technologie anzuwenden, muss im Hauptmodule (app.module.ts) ReactiveFormsModule hinzugefügt werden.
Im Modell wird die FormGroup instanziert. Sie dient als Bindeglied zum Formular.
import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; import { EmailService } from '../../service/email.service'; import { UniqueEmailValidatorDirective, UniqueEmailValidator } from '../../directiven/emailUnique.directive'; @Component({ selector: 'app-reaktiv-form', templateUrl: './reaktiv-form.component.html', styleUrls: ['./reaktiv-form.component.css'] }) export class ReaktivFormComponent { genders = ['Herr', 'Frau']; customer = { firstname: 'Hans', email: '[email protected]', lastname: 'Muster', gender: this.genders[0] }; customerForm: FormGroup; constructor(private fb: FormBuilder, private emailService: EmailService, private uniqueEmailValidator: UniqueEmailValidator) { this.customerForm = fb.group({ email: [this.customer.email, [Validators.required, Validators.email], this.uniqueEmailValidator.validate.bind(this.uniqueEmailValidator) ], gender: [this.customer.gender, [Validators.required]], firstname: [this.customer.firstname, [Validators.required, Validators.minLength(5)]], lastname: [this.customer.lastname, [Validators.required]], }) } get firstname() { return this.customerForm.get('firstname'); } get lastname() { return this.customerForm.get('lastname'); } get email() { return this.customerForm.get('email'); } get gender() { return this.customerForm.get('gender'); } }
reaktiv-form.component.ts
Die durch NgForm instantierte FormGroup der obersten Ebene verbindet das Formular, um den aggregierten Formularwert und den Überprüfungsstatus – wie zuvor beschrieben – zu verfolgen.
customerFormGroup stellt somit für jedes Feld ein FormControl mit den gewünschten Validierungs- und Statiprüfungen zur Verfügung. Somit aggregiert Formgroup den Zustand von allen FormControls. Ist ein FormControl nicht valide, ist auch FormGroup nicht valide.
. Dabei sieht der Aufbau im Formular wie folgt aus:
<h1>Reactive Form</h1> <form novalidate [formGroup]="customerForm"> <div class="form-group"> <label for="gender">Gender:</label> <select id="gender" class="form-control" formControlName="gender" required> <option *ngFor="let p of genders" [value]="p">{{p}}</option> </select> <div *ngIf="gender.invalid && gender.touched" class="alert alert-danger"> <div *ngIf="gender.errors.required">Gender is required.</div> </div> </div> <div class="form-group"> <label for="email">Email:</label> <input type="email" id="email" class="form-control" formControlName="email"> <div *ngIf="email.pending">Validating...</div> <div *ngIf="email.invalid" class="alert alert-danger email-errors"> <div *ngIf="email.errors?.uniqueEmail==true">Already registered email</div> <div *ngIf="email.errors?.minlength">email must be at least 4 characters long.</div> <div *ngIf="email.errors?.email">does not have the pattern of an email address.</div> <div *ngIf="email.errors.required">Email name is required.</div> <!--Alternative Möglichkeit, auf das Model zurück zu greifen--> <div *ngIf="customerForm.get('email').errors && customerForm.get('email').errors.uniqueEmail"> Already registered!! </div> </div> </div> <div class="form-group"> <label for="firstname">First name:</label> <input id="firstname" class="form-control" formControlName="firstname" required> <div *ngIf="firstname.invalid && (firstname.dirty || firstname.touched)" class="alert alert-danger"> <div *ngIf="firstname.errors.required"> First name is required. </div> <div *ngIf="firstname.errors.minlength"> First name must be at least 4 characters long. </div> </div> </div> <div class="form-group"> <label for="lastname">Last name:</label> <input id="lastname" class="form-control" formControlName="lastname"> <div *ngIf="lastname.pending">be patient, we check your data...</div> <div *ngIf="lastname.invalid" class="alert alert-danger last-name-errors"> <div *ngIf="lastname.errors.required"> Last name is required. </div> <div *ngIf="lastname.errors.minlength"> Last name must be at least 4 characters long. </div> </div> </div> <button type="submit" class="btn btn-default" [disabled]="customerForm.invalid"> Submit </button> </form>
Im Model muss nur noch ein Getter wie folgt zur Verfügung gestellt werden,
get email() { return this.customerForm.get('email'); }
um auf das entsprechende Objekt im Form zuzugreifen.
<div *ngIf="lastname.invalid" class="alert alert-danger last-name-errors">
Auf diesen Getter kann verzichtet, jedoch müsste aber folgender Schreibweise angewendet werden.
<div *ngIf="customerForm.get('email').errors && customerForm.get('email').errors.uniqueEmail"> Already registered!! </div>
Dadurch wird jedoch das Form eher unleserlich.
Zentral von Bedeutung bei der Validierung von Daten ist beim reaktiven Modell die Klasse Formgroup. Wie erwähnt, Formgroup ist das Bindeglied zu ngForm-Direktive und aggregiert sämtliche zugewiesenen Formcontrols. (siehe reaktiv-form.component.ts).
Im Überblick sind hier die wichtigsten Properties und Methoden aufgeführt.
Properties/Methode | Bedeutung |
submitted | Gibt zurück, ob eine Form-Übermittlung ausgelöst wurde |
errors | Gibt an, ob ein Form fehlerhaft ist. Tritt mindestens ein Fehler in einem Eingabefeld auf, hat die Variable den Wert «true» |
touched | Gibt an, ob ein Eingabefeld den Fokus erhielt oder ein Mouse-Event ausgelöst wurde. |
dirty | Wird ein Wert in einem Eingabefeld nach der Eingabe verändert, hat die Variable «dirty» den Wert true |
resetForm() | Alle Eingabefelder werden zurückgesetzt. |
controls | Gibt ein Instanz der FormControl aus der FormGroup zurück. |
Weitere detaillierte Infos sind hier zu finden.
Die Technologie Reactive-Forms wirkt aus meiner Sicht übersichtlicher und stellt für aufwendigere Validierungen die bessere Lösung dar. Dieser Ansatz kommt vor allem dann zum Tragen, wenn Forms dynamisch geladen werden oder eine Applikation mehrsprachig funktionieren muss.
Schreiben Sie einen Kommentar