Feld Validierungen mit Angular FormBuilder sind einfach. Sie werden einfach bei der Feld-Definition angegeben, z.B. eine Minimal-Länge beim Passwort.
1 2 |
this.passwordGroup = this.formBuilder.group({ 'password': ['', [Validators.required, Validators.minLength(6)]], |
Die Validierung akzeptiert übrigens auch einen Array von Validators. Wie im Beispiel gezeigt, werden zwei Validators übergeben (required und minLength).
Die Validierung kann kann dann im UI verwendet werden.
1 2 3 4 5 6 |
<ion-item [class.error]="!password.valid && password.touched"> <ion-label floating>Password</ion-label> <ion-input type="password" name="password" [formControl]="password" spellcheck="false" autocapitalize="off"></ion-input> </ion-item> <div *ngIf="password.hasError('required') && password.touched" class="error-box">Password is required</div> <div *ngIf="password.hasError('minlength') && password.touched" class="error-box">Password minimum Length is 6</div> |
Asynchrone Validierung
Nebst dieser klassischen, synchronen Validierung können auch asynchrone Validierungen gemacht gemacht werden. Gerade mit Angular, Ionic und Firebase, wo alles asynchrone mit Observables abläuft, wird man früher oder später dazu kommen. Allerdings habe ich nirgends ein richtiges Beispiel dazu gefunden. Alle Tutorials verwenden immer irgendwelche Observables mit mit setTimeout.
In meinem Beispiel musste ich bei der Benutzer-Registrierung die Email-Adresse prüfen, ob sie schon verwendet wurde. Ein klarer Fall für eine asynchrone Validierung. Der Validator wird als drittes Argument nach den synchronen Validators angegeben.
1 2 3 4 5 |
this.registerForm = this.formBuilder.group({ 'username': ['', Validators.required], 'email': ['',[Validators.required, EmailValidator.valid], this.validateUniqueEmail.bind(this)], 'passwords': this.passwordGroup }); |
Damit der asynchrone Validator auf die Instanz-Variablen zugreifen kann, wird die Methode mit .bind(this) verbunden.
Die einfachste Möglichkeit die Validierung asynchron durchzuführen, ist eine Promise zu verwenden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
validateUniqueEmail(formControl: FormControl) { let q = new Promise((resolve, reject) => { let exists = []; this.userService.users$.first().subscribe((users) => { exists = users.filter(user => user.email.toLowerCase() == formControl.value.toLowerCase()) }); if (exists.length > 0) { resolve({emailAddressAlreadyRegistered: true}) } else { resolve(null); } }); return q; } |
userService.users$ ist in diesem Fall ein Firebase-Zugriff, es könnte aber genau so gut ein http-Aufruf mit JSON-Rückgabe sein.
1 2 3 4 5 6 7 8 |
@Injectable() export class UserService { private usersRef: string = 'users'; public get users$(): Observable<any[]> { return this.af.database.list(`/${this.usersRef}`) } |
Ausstehende Validierung
Wie weiss das UI nun, ob die asynchrone Validierung beendet ist? Angular liefert dazu bei asynchronen Validators den Status „pending“.
1 2 3 4 5 6 7 |
<ion-item [class.error]="!email.valid && !email.pending"> <ion-label floating>Email<span *ngIf="email.pending"> (checking...)</span></ion-label> <ion-input type="email" name="email" [formControl]="email" spellcheck="false" autocapitalize="off"></ion-input> </ion-item> <div *ngIf="email.hasError('required') && email.touched" class="error-box">Email is required</div> <div *ngIf="email.hasError('validateEmail') && email.touched" class="error-box">Invalid Email address</div> <div *ngIf="email.hasError('emailAddressAlreadyRegistered') && !email.pending" class="error-box">Email not allowed.</div> |
So lange übrigens eine Validierung aussteht, kann auch das ganze Form nicht valid sein. Das heisst, mann muss z.B. beim Submit-Button nicht noch extra auf „pending“ prüfen.
1 |
<button ion-button block type="submit" [disabled]="!registerForm.valid">Register</button> |
Wiederverwendbare Validators
Die Validierung in der gleichen Klasse zu definieren, wie die zu testenden Felder, geht so lange, wie die Validierung nicht mehrmals verwendet werden muss.
Typischerweise hat man für mehrfach verwendete Validatoren eine eigene Klasse, wie z.B. für Email-Adressen (wofür es erstaunlicherweise keinen Default-Validator in Angular gibt)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { FormControl } from '@angular/forms'; export class EmailValidator { static valid(formControl: FormControl) { let EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; return EMAIL_REGEXP.test(formControl.value) ? null : { validateEmail: { valid: false } } } } |
Der Client importiert dann den Validator und verwendet ihn wie einen Standard Validator
1 2 3 |
import { EmailValidator } from '../../validators/email.validator'; ... 'email': ['',[Validators.required, EmailValidator.valid]], |
Wenn nun asynchrone Validators in einer eigenen Klasse definiert werden sollten, dann funktioniert der „.bind(this)“ Trick nicht mehr.
In diesem Fall müsste die Validator-Klasse die nötigen Services, wie z.B. oben der Firebase UserService selbst instanzieren. Da alle Methoden aber typischerweise static sind, können die benötigen Services nicht wie gewohnt im Constructor injected werden. Deshalb nimmt man den ReflectiveInjector dazu.
1 2 3 4 5 6 7 8 9 10 11 12 |
import { UserService } from '../../providers/user.service'; let injector = ReflectiveInjector.resolveAndCreate([UserService]); let userService = injector.get(UserService); let q = new Promise((resolve, reject) => { let exists = []; userService.users$.first().subscribe((users) => { exists = users.filter(user => user.email.toLowerCase() == formControl.value.toLowerCase()) }); ... |