Multi-Field-Validatoren Mit Angular 2

Multi-Field-Validatoren Mit Angular 2

Dieser Beitrag bezieht sich auf Angular 2 Beta 0. Bei künftigen Releases kann es zu Änderungen kommen.

Zum Validieren von Eingaben mit Angular 2 weist der Entwickler Validatoren zu den einzelnen Controls zu. Gilt es jedoch mehrere Controls zu berücksichtigen, kann er diese auch bei ControlGroups registrieren. In diesem Fall hat der Validator Zugriff auf sämtliche Controls dieser Gruppe.

Das nächste Beispiel zeigt solch einen Validator in Form der statischen Methode validate. Sie nimmt eine ControlGroup entgegen und sucht innerhalb dieser nach den Feldern von und nach. Sind diese (noch) nicht vorhanden, beendet sie die Validierung ohne Fehler. Ansonsten prüft sie, ob die beiden Felder korrekte Werte beinhalten. Im Fehlerfall liefert sie ein Fehlerbeschreibungsobjekt mit dem Wert route: true zurück; sonst ein leeres Objekt um den Erfolg der Validierung anzuzeigen.

export class RouteValidator {

    static validate(group: ControlGroup): any {

        var von = group.find('von');
        var nach = group.find('nach');

        if (!von || !nach) return {};

        if (von.value == 'Graz' && nach.value == 'Hamburg') {
            return {};
        }
        return {
            route: true
        }        
    }

}

Einsatz mit imperativem Forms-Handling

Kommen imperative Formulare zum Einsatz, steht die ControlGroup direkt zur Verfügung. Ihre Eigenschaft validator nimmt einen Validator entgegen (siehe nächstes Beispiel). Möchte der Entwickler mehrere Validatoren zuweisen, kann er diese mit der Methode Validators.compose zu einem einzigen übergeordneten Validiator zusammenführen und diesen anschließend an die erwähnte Eigenschaft zuweisen.

@Component({
    selector: 'flug-suchen',
    templateUrl: 'app/flug-suchen2/flug-suchen2.html',
    directives: [CORE_DIRECTIVES, FORM_DIRECTIVES],
    pipes: [OrtPipe]
})
export class FlugSuchen2 {

    fluege = [];
    selectedFlug;
    flugService: FlugService;
    filter: ControlGroup;

    constructor(flugService: FlugService, fb: FormBuilder) {
        this.flugService = flugService;

        this.filter = fb.group({
            von: ['Graz'],
            nach: ['Hamburg'],
            maxSegmente: ['2']
        });

        this.filter.validator = RouteValidator.validate;

    }

    [...]
}

Die View bindet die bereitgestellte ControlGroup - wie gewohnt - über [ngFormModel]. Da der Validator für die gesamte ControlGroup gilt, muss die View eventuelle Fehlermeldungen auch auf dieser Ebene in Erfahrung bringen. Dazu ruft sie im nachfolgenden Bespiel die Methode filder.hasError auf.

<form [ngFormModel]="filter">

    <div *ngIf="filter.hasError('route')">
        Diese Route wird nicht angeboten!
    </div>

    <div class="form-group">
        <label>Von</label>
        <input ngControl="von" class="form-control">
    </div>

    <div class="form-group">
        <label>Nach</label>
        <input ngControl="nach" class="form-control">
    </div>

    <div class="form-group">
        <label>Max. Segmente</label>
        <input ngControl="maxSegmente" class="form-control">
    </div>

    [...]

</form>

Einsatz mit deklarativem Forms-Handling

Für den Einsatz mit deklarativen Formularen ist eine Direktive einzurichten. Zusätzlich benötigt man dafür auch eine Methode validate, welche eine ControlGroup entgegennimmt und validiert. An und für sich ist es egal, in welcher Klasse sich diese Methode wiederfindet - die Direktive muss lediglich darauf verweisen. Zur Vereinfachung platziert das nachfolgende Beispiel diese Methode direkt in der Direktive.

Um auf die validate-Methode zu verweisen, nutzt die Direktive einen Provider. Dieser bindet dessen Klasse an das von Angular 2 vorgegebene Token NG_VALIDATORS. Hierzu kommt ein Multi-Binding zum Einsatz. Der Selektor gibt an, wie die View die Direktive nutzen kann. Im betrachteten Fall verknüpft er sie mit Elementen, die das Attribut route aufweisen.

@Directive({
    selector: '[route]', 
    providers: [provide(NG_VALIDATORS, {useExisting: RouteValidatorDirective, multi: true})]
})
export class RouteValidatorDirective {

    validate(group: ControlGroup) {
        return RouteValidator.validate(group);
    }
} 

Für die Methode validate bietet Angular 2 auch das Interface Validator an. Da diese jedoch die Validierung eines Controls vorsieht und ControlGroup dazu nicht zuweisungskompatibel ist, kann es hier nicht zum Einsatz kommen. Da Interfaces ohnehin beim Kompilieren verschwinden und Angular 2 zur Laufzeit somit keine Kenntnis davon hat, ist dies nicht weiter schlimm.

Damit die Direktive in der gewünschten View zur Verfügung steht, ist sie - wie gewohnt - zu registrieren. Dies kann zum Beispiel über die Eigenschaft directives des Component-Dekorators erfolgen.

@Component({ 
    selector: 'flug-suchen',
    templateUrl: 'app/flug-suchen/flug-suchen.html',
    directives: [
        RouteValidatorDirective
    ]
})
export class FlugSuchen {
    [...]
}

Die View markiert in weiterer Folge das zu validierende Formular mit der Direktive. Dazu spendiert sie ihm das Attribut route. Danach kann sie bei jener ControlGroup, mit dem die FormDirective das gesamte Formular repräsentiert, eine eventuelle Fehlermeldung abrufen. Diese findet sich in der Eigenschaft form:

<form #f="ngForm" route>

    <div *ngIf="f.form.hasError('route')">
        Diese Route wird nicht angeboten!
    </div>

    <div class="form-group">
        <label>Von</label>
        <input [(ngModel)]="von" name="von" ngControl="von" ort required class="form-control">
    </div>

    <div class="form-group">
        <label>Nach</label>
        <input [(ngModel)]="nach" ortAsync name="nach" ngControl="nach" class="form-control">
    </div>

    <div class="form-group">
        <label>Max. Segmente</label>
        <input [(ngModel)]="maxSegmente" ngControl="maxSegmente" class="form-control" min="1" max="5">
    </div>

    [...]

</form>