Formulare sind der Kern vieler Businessanwendungen. Neben der Prüfung innerhalb des Backends ist es in aller Regel auch eine Anforderung den Benutzer bereits bei der Eingabe auf eventuelle Fehler hinzuweisen. Angular bietet hierfür ein Konzept, das auf Direktiven basiert.
Die von Angular mitgelieferten Validatoren sind überschaubar:
- required, Pflichtfeld
- minlength, Mindestlänge
- maxlength, Maximallänge
- pattern, Regular-Expression
Die Verwendung dieser Validatoren ist denkbar einfach. Nachdem das “FormsModule” (import { FormsModule } from ‘@angular/forms’;) in unser Modul importiert wurde, können diese Validatoren mittels Direktive an unseren Eingabefeldern registriert werden. Der Zugriff auf eventuelle Fehler erfolgt über den Zugriff mittels Templatevariablen (‘ngForm’ für Formulare, ‘ngControlGroup’ für Gruppen, ‘ngModel’ für Eingabefelder) und z.B. mit der Verwendung der “valid” Eigenschaft.
<form #form="ngForm" (ngSubmit)="submit()" novalidate> <span *ngIf="!namefield.valid"> Pflichtfeld </span> <input name="name" type="text" class="form-control" required ngModel #namefield="ngModel"> <button type="submit" [disabled]="!form.valid">submit</button> </form>
Eigene Validatoren
Hierzu wird eine Direktive erstellt die mit Hilfe eine so genannten “Multi Providers” den bestehenden Validatoren (NG_VALIDATORS) hinzugefügt wird (multi=true). Die eigentliche Implementierung erfolgt in der “validate” Methode, die entweder null zurück liefert, wenn kein Fehlerfall besteht oder ein Objekt, welches weitere Informationen zur Anzeige bereit stellen kann. Neben einzelnen Feldern können so auch Felderübergreifende Prüfungen implementiert werden. Anstatt die Direktive auf einem einzelnen Feld ein zu setzen wird dann im Template eine FromGroup verwendet die dem Validator später übergeben wird. Hier das Beispiel eines solchen Validators der über zwei Felder hinweg prüft ob ein gültiger numerischer Bereich angegeben wurde. Hier sehen wir auch wie unser Validator mit Parametern konfiguriert werden kann, in diesem Fall benötigt er die Pfade zu den Form-Controls um diese aus der FormGroup zu erhalten.
app.component.html
<div ngModelGroup #range="ngModelGroup" appRangeValidator von-pfad="lvon" bis-pfad="lbis"> <input name="lvon" type="text" ngModel> <input name="lbis" type="text" ngModel></div> {{range?.errors | json}}
range-validator.directive.ts
import { Directive, Input } from '@angular/core'; import { NG_VALIDATORS, FormGroup } from '@angular/forms'; @Directive({ selector: '[appRangeValidator]', providers: [ { provide: NG_VALIDATORS, useExisting: RangeValidatorDirective, multi: true } ] }) export class RangeValidatorDirective { @Input('von-pfad') path1: string; @Input('bis-pfad') path2: string; validate(element: FormGroup) { const [val1, val2] = [element.get(this.path1), element.get(this.path2)]; if ([val1, val2].every(e => e && e.value && e.value !== '')) { if ([val1.value, val2.value].some(e => !/^\d+$/.test(e))) { return { 'invalid-error': { valid: false, message: 'Ungültige Eingabe' } }; } else if (Number.parseInt(val1.value) > Number.parseInt(val2.value)) { return { 'range-error': { valid: false, message: 'Erste Zahl muß kleiner sein als die Zweite' } }; } } return null; } }
Asyncrone Validatoren
Neben dem gerade gezeigtem syncronen Validatore bietet Angular auch eine asycrone Möglichkeit Prüfungen durch zu führen, wenn die zu prüfenden Werte z.B. erst an eine Rest-Schnittstelle übertragen werden sollen. Der grundsätzliche Aufbau bleibt der gleiche, wir verwenden lediglich den Multi Provider “NG_ASYNC_VALIDATORS” bei der Registrierung und unsere “validate” Methode muss dann ein Promise oder Observable zurück liefern, das wiederrum “null” oder ein Objekt mit den Fehler informationen enthält
async-user-validator.direvtive.ts
import { Directive } from '@angular/core'; import { NG_ASYNC_VALIDATORS, AbstractControl } from '@angular/forms'; import { Http } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/Rx'; @Directive({ selector: '[appAsyncUserValidator]', providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: AsyncUserValidatorDirective, multi: true } ] }) export class AsyncUserValidatorDirective { constructor(private http: Http) { } validate(element: AbstractControl) { if (element && element.value && element.value !== '') { return this.http .get('https://jsonplaceholder.typicode.com/users') .map(r => r.json()) .first() .map((e: any[]) => e.some(ele => { return ele.id == element.value })) .map(e => e ? null : { 'usernot': 'Benutzer existiert nicht' }); } return Observable.from([null]); } }
Der Validator fragt eine Rest-Schnittstelle nach Userdaten ab die als Rückgabe ein Array mit Usern liefert (für diesen Fall zugegeben ein schlechtes Schnittstellen-Design). Wir wandeln die Response hier in ein JSON-Objekt um, lassen uns das erste Elemente ( das Array ) geben, prüfen ob mindestens einer der User die übergebene ID besitzt und basierend auf diesem true/false Ergebnis erzeugen wir bei Bedarf einen entsprechenden Fehler.
(In einer echten Anwendung gehört die Kommunikation mit der Rest-Schnittstelle selbstverständlich in einen separaten Service!)
Zugegeben die Validatoren die Angular mitliefert sind überschaubar. Aber dank Bordmittel sind individuelle Validatoren schnell implementiert und auf die eigenen Bedürfnisse anpassbar.