Suche
Tutorial Teil 2: Formulare und Validierung

Angular 2 Tutorial für Einsteiger: Formsache

Manfred Steyer

(c) Shutterstock / gdaint

Dank Two-Way-Binding gestaltet sich die Arbeit mit Formularen in Angular 2 sehr einfach. Zum Prüfen der Eingaben unterstützt Googles SPA-Framework sowohl synchrone als auch asynchrone Validierungsregeln. Im zweiten Teil unseres Angular-2-Tutorials wollen wir uns das Erstellen von Formularen genauer ansehen.

Dieses Angular 2 Tutorial beschäftigt sich mit dem Aufbau einer Single-Page-Anwendung mit Angular 2 und TypeScript. Der Single-Page-Ansatz erlaubt die Entwicklung von webbasierten Frontends, die auf sämtlichen klassischen und mobilen Plattformen laufen und durch den Einsatz von JavaScript einen hohen Grad an Benutzerfreundlichkeit bieten. Das Framework Angular 2 unterstützt das und ermöglicht das Erreichen einer hohen Codequalität mit bewährten Konzepten, wie Komponentenorientierung und Dependency Injection.

Das große Angular 2 Tutorial

Teil 1: Erste Schritte mit Angular 2 und TypeScript
Teil 2: Formulare und Validierung

Teil 3: Navigation mit dem Component Router

Angular 2 Tutorial: Formsache

Eine Stärke von Angular ist seit jeher die Verwaltung von Formularen. Wie auch schon der Vorgänger AngularJS 1.x bietet Angular 2 hierzu eine Direktive ngModel. Sie kümmert sich um ein Two-Way-Binding, welches die Werte der Felder mit denen der dahinterstehenden Objekteigenschaften abgleicht. Änderungen im Objekt übernimmt sie in das Formular, und Änderungen im Formular sendet sie nach einer erfolgreichen Validierung ans Objekt zurück. Somit muss sich der Entwickler nicht mit dieser monotonen und fehleranfälligen Aufgabe belasten. Zur Validierung stellt Angular einige Standardvalidierungsregeln, zum Beispiel solche für Pflichtfelder, zur Verfügung. Daneben kann eine Anwendung auch eigene Regeln bereitstellen und ins Forms-Handling einklinken.

Dieser Artikel geht hierauf ein. Dazu erweitert er das in diesem Tutorial verwendete Beispiel um Validierungsregeln für das Formular zu Suchen nach Flügen (Abb. 1). Der gesamte Quellcode findet sich unter [1].

angular2-form-sample

Abb. 1: Formular mit Validierungsfehler

w-jax 2016 logoTreffen Sie Manfred Steyer auf der W-JAX 2016

Session: Offlinefähige Browseranwendungen: Progressive Web-Apps mit Angular 2.0

Die so genannten „progressiven Web-Apps“ sind die nächste Stufe moderner Webanwendungen. Sie verhalten sich noch stärker wie native Anwendungen und unterstützen dank neuester Browserstandards Offlinefähigkeit, echte Benachrichtigungen sowie Datensynchronisation im Hintergrund. Dazu kommen Ansätze wie Service Worker, das Web App Manifest, Browserdatenbanken, aber auch das App-Shell-Muster zum Einsatz. Diese Keynote geht auf diesen neuen Architekturansatz, der bereits von nationalen und internationalen Konzernen aufgegriffen wurde, ein und zeigt am Beispiel einer Angular-2-Anwendung, wie dieser Ansatz realisiert werden kann und worauf dabei zu achten ist.

Mittwoch, 9.11.2016 – 11:45 – 12:45 Uhr

Erste Schritte mit Formularen

Um Formulare zu nutzen, referenziert die Anwendung zunächst das FormsModule von Angular (Listing 1).

import {FormsModule} from "@angular/forms";
[…]
@NgModule({
        imports: [BrowserModule, HttpModule, FormsModule, […] ], […]
    })
    export class AppModule { }

Bei den meisten Seed-Projekten ist dies von Anfang an der Fall. Auch das hier beschriebene Tutorial nutzt dieses Modul seit dem ersten Teil. Danach bindet der Entwickler Eingabesteuerelemente mittels ngModel und Two-Way-Data-Binding an die jeweiligen Eigenschaften:

<input class=“form-control“ [(ngModel)]=“von“ name=“von“>

Angular erzwingt dabei, dass das Eingabefeld einen Namen bekommt. Zusätzlich besteht die Möglichkeit, Validierungsregeln über Attribute anzugeben. Das nachfolgende Beispiel zeichnet auf diese Weise das Eingabefeld als Pflichtfeld mit mindestens drei Zeichen aus:

<input class=“form-control“ [(ngModel)]=“von“ name=“von“
required minlength=“3″>

Einen Überblick über die von Angular 2 angebotenen Validierungsregeln findet sich in Tabelle 1. Wer damit nicht auskommt, kann auch – wie weiter unten beschrieben – eigene Validierungsregeln implementieren.

Klasse Zustand
required Pflichtfeld
minlength Minimale String-Länge
maxlength Maximale String-Länge
pattern Prüfung gegen regulären Ausdruck

Tabelle 1: Build-in Validatoren

Zugriff auf Zustand des Formulars

Im Hintergrund erzeugt Angular einen Objektgraphen, der das gesamte Formular beschreibt (Abb. 2). An der Spitze dieses baumförmigen Graphen findet sich ein Objekt vom Typ FormDirective. Es steht für das gesamte Formular und zeigt unter anderem an, ob es korrekt validiert (valid) oder verändert (dirty) wurde. Nur wenn sämtliche Felder des Formulars korrekt validiert wurden, sieht Angular das Formular als valide an. Wurde auch nur ein Feld verändert, sieht Angular das Formular hingegen als verändert an.

angular2-form-controller

Abb. 2: Objektgraph zur Beschreibung eines Formulars

Der Objektgraph enthält pro Eingabefeld ebenfalls ein Objekt. Diese lassen sich über die Auflistung Controls der FormDirective erreichen. Sie spiegeln den Zustand der einzelnen Eingabefelder wider und geben ebenfalls Auskunft über den Ausgang der Validierung (valid) sowie darüber, ob das Feld einer Änderung unterlag (dirty).

Um eine Referenz auf diese Objekte zu erhalten, führt der Entwickler ein so genanntes Handle für das Formular ein. Dabei handelt es sich um eine Templatevariable, die auf eine hinter diesem Element stehende Direktive oder Komponente verweist. Ein Beispiel dafür findet sich in Listing 2. Es deklariert für das form-Element ein Handle f. Im Zug der Deklaration ist dem Handle eine Raute (#) voranzustellen. Diese ist bei weiterer Nutzung des Handles nicht mehr anzuführen.

<form #f="ngForm">

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

    <div *ngIf="!f?.controls?.von?.valid">
      Es liegen Validierungsfehler für diese Eingabe vor.
    </div>
    <div *ngIf="f?.controls?.von?.hasError('required')">
      Dieses Feld ist ein Pflichtfeld.
    </div>
    <div *ngIf="f?.controls?.von?.hasError('minlength')">
      Erfassen Sie bitte min. 3 Zeichen.
    </div>
  </div>

  [...]

  <div>
    <input type="button" [disabled]="!f.valid" (click)="search()" 
               class="btn" value="Suchen" />
  </div>

</form>

Da hinter einem HTML-Element neben einer Komponente mehrere Direktiven stehen können, weist das betrachtete Beispiel zu #f den Wert ngForm zu. Damit gibt es an, dass die vorhin erwähnte FormsDirective zu f zuzuweisen ist. Dies funktioniert, da das Angular-Team die FormsDirective unter diesem Kürzel veröffentlicht.

Ein input-Element bindet das Beispiel an die Eigenschaft von, die sich im Component-Controller befindet (hier nicht abgebildet). Es weist die Validierungsattribute required und minlength auf. Daneben gibt das Attribut name an, dass dieses Eingabeelement im Objektgraphen durch ein Objekt mit dem Namen von zu repräsentieren ist. Somit kann der Entwickler darauf über die FormsDirective mit f.controls.von zugreifen. Da bei der deklarativen Vorgehensweise Angular den Objektgraphen erst nach der initialen Datenbindung erzeugt, sollte zusätzlich der Safe-Access-Operator (Elvis-Operator) zum Einsatz kommen: f?.controls?.von. Das dem Punkt vorangestellte Fragezeichen bewirkt, dass Angular den gesamten Ausdruck als null auswertet, wenn der Teil links davon null ist. Auf diese Weise verhindert es die Navigation über null hinweg, was Angular 2 im Gegensatz zu seinem Vorgänger mit einer entsprechenden Exception abmahnt.

Das Beispiel prüft mittels *ngIf und der Eigenschaft valid, ob für den Von mindestens ein Validierungsfehler vorliegt. In diesem Fall gibt es eine Fehlermeldung aus. Zusätzlich prüft es, welcher Validierungsfehler vorliegt. Dazu kommt die Methode hasError zum Einsatz. Auf diese Weise gibt das Beispiel eine zusätzliche, zu den aufgetretenen Fehlern passende Information aus.

Am Ende des Beispiels befindet sich auch eine Schaltfläche. Die Bindung an das Attribut disabled bewirkt, dass Angular sie beim Vorliegen von Validierungsfehlern deaktiviert.

Handles für Eingabefelder
Der Entwickler kann auch für Eingabeelemente Handles einführen. Dies verkürzt die Länge der einzelnen Ausdrücke:

<input #von
  class="form-control" 
  [(ngModel)]="von" 
  required minlength="3" 
  name="von" />

<div *ngIf="von?.valid">
  Validierungsfehler!
</div>

Bedingte Formatierung von Eingabefeldern

Abhängig vom aktuellen Zustand weist Angular den Eingabefeldern unterschiedliche Klassen zu. Diese kann eine Anwendung für bedingte Formatierungen nutzen, indem sie für diese Klassen Formatierungen mittels CSS bereitstellt. Tabelle 1 bietet einen Überblick hierzu.

Klasse Zustand
ng-invalid Das Feld wurde nicht korrekt validiert
ng-valid Für das Feld liegt kein Validierungsfehler vor
ng-dirty Das Feld wurde vom Benutzer verändert
ng-pristine Das Feld wurde vom Benutzer nicht verändert

Tabelle 1: Klassen für Formularfelder

Um nun Eingabefeldern mit fehlerhaften Eingaben einen roten und solchen mit korrekten Eingaben einen grünen Rand zu verpassen, kann die Anwendung das unter Listing 3 gezeigte Stylesheet verwenden.

input.ng-invalid {
    border-left-color: red;
    border-left-style: solid;
    border-left-width: 5px;
}

input.ng-valid {
    border-left-color: green;
    border-left-style: solid;
    border-left-width: 5px;
}

Um dieses Stylesheet zu referenzieren, bietet sich die Eigenschaft styles im Component-Dekorator an (Listing 4).

@Component({
    selector: 'flug-suchen',
    template: 'app/flug/flug-suchen/flug-suchen.component.html',
    styles: ['app/flug/flug-suchen.component.css']
})
export class FlugSuchenComponent {
    […]
}

Das Besondere an dieser Lösung ist, dass Angular diese Styles auf die aktuelle Komponente isoliert. Somit läuft die Anwendung nicht Gefahr, dass sich Styles unterschiedlicher Komponenten in die Quere kommen. Zur Realisierung nutzt Angular die mit Web Components assoziierte Browsertechnologie Shadow Dom. Für Browser, die Shadow Dom noch nicht unterstützen, emuliert das Framework dessen Verhalten.

Eigene Validierungsdirektiven

Eigene Validierungsregeln kann die Anwendung über Direktiven bereitstellen. Diese erweitern, wie zum Beispiel ngModel auch, Elemente der Anwendung um zusätzliches Verhalten. Zur Demonstration soll die hier präsentierte Demo-Anwendung eine solche Validierungsdirekte nutzen, die die erfassten Orte (Von und Nach) validiert. Diese soll in Form eines Attributs ort beim jeweiligen input-Element aktiviert werden können:

<input class="form-control" 
  [(ngModel)]="von" 
  required minlength="3" maxlength="30" 
  ort 
  name="von" />

<div *ngIf="f?.controls?.von?.hasError('ort')">
  Dieser Flughafen existiert nicht
</div>

Bei Direktiven handelt es sich um Klassen, die mit Directive dekoriert werden. Für Validierungsdirektiven stellt Angular zusätzlich das Interface Validator mit der Methode validate zur Verfügung (Listing 5).

import { Directive, Attribute, forwardRef } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';

@Directive({
    selector: 'input[ort]',
    providers: [
        {
            provide: NG_VALIDATORS,
            useExisting: forwardRef(() => OrtValidatorDirective),
            multi: true
        }
    ]
})
export class OrtValidatorDirective implements Validator{

    public validate(c: AbstractControl): any {

        if (c.value == 'Graz'
            || c.value == 'Hamburg'
            || c.value == 'Frankfurt'
            || c.value == 'Wien'
            || c.value == 'Mallorca') {

            return {};
        }

        return {
            ort: true
        }
    }
}

 

Die Methode validate nimmt ein AbstractControl, welches das zu validierende Steuerelement repräsentiert, entgegen. Die betrachtete Implementierung prüft, ob es sich bei seinem Wert um einen bekannten Ort handelt. Wurde der Wert korrekt validiert, liefert sie ein leeres Fehlerbeschreibungsobjekt. Ansonsten retourniert es ein Fehlerbeschreibungsobjekt, das mit der Property ort auf die fehlgeschlagene Validierung hinweist. Die Anwendung kann später prüfen, ob ein durch solch einen Namen ausgedrückter Fehler vorliegt. Die Werte solcher Eigenschaften sind für Angular unerheblich. Allerdings kann sie die Anwendung zur Erstellung einer entsprechenden Fehlerausgabe nutzen.

Der Dekorator Directive legt einen CSS-Selektor fest. Dieser verweist auf die Elemente, für die er aktiv wird. Demnach ist die Direktive auf alle input-Elemente mit einem Attribut ort anzuwenden. Um Angular mitzuteilen, dass diese Direktive eine Validierungsdirektive für das aktuelle Feld ist, muss die Anwendung über den Dekorator einen Provider definieren. Dieser bindet das vom Framework vorgegebene Token NG_VALIDATORS mit useExisting an die aktuelle Instanz der Direktive. Da es für ein Feld mehrere Validierungsdirektiven geben kann, ist die Eigenschaft multi auf true zu setzen. Zur Laufzeit besorgt sich Angular pro Feld sämtliche Validatoren, welche mit diesem Token verbunden sind, und führt sie zur Prüfung der Eingaben aus.

Bei Betrachtung der Provider-Einstellung fällt auch auf, dass der Dekorator Directive auf eine Klasse verweist, die der Quellcode erst einige Zeilen weiter unten deklariert. Deswegen kommt hier eine Forward Reference, die zur Laufzeit über einen Lambda-Ausdruck die Klasse retourniert, zum Einsatz.

Provider für Validierungsfunktion
Als Alternative zur Nutzung eines Providers, der NG_VALIDATORS an eine Klasse mit einer validate-Methode bindet, könnte die Anwendung auch Gebrauch von einem Provider machen, der NG_VALIDATORS direkt an die Validierungsfunktion bindet.

@Directive({
  selector: 'input[ort]', 
  providers: [provide(NG_VALIDATORS,
    { useValue: OrtValidator.someStaticValidateFunction, multi: true })]
})

 

Damit Angular eine Direktive zur Laufzeit berücksichtigt, ist sie bei einem Modul zu registrieren. Wie auch im Falle von Komponenten ist sie dazu unter declarations einzutragen (Listing 6).

@NgModule({
imports: [
        BrowserModule,
        HttpModule,
        FormsModule
    ],
    declarations: [
        FlugSuchenComponent,
        OrtValidatorDirective
    ],
    providers: [
        FlugService
    ],
    bootstrap: [
        FlugSuchenComponent
    ]
})
export class AppModule { 
}

Parametrisierbare Direktiven

Die im letzten Abschnitt betrachtete Validierungsdirektive prüft gegen hartcodierte Werte. Die Lösung wäre flexibler, wenn sie gegen eine Liste frei definierbarer Einträge validieren würde:

<input
        [(ngModel)]="von"
        name="von"
        class="form-control"
        required
        ort="Graz,München,Hamburg,Frankfurt,Zürich,Wien"
        minlength="3"
        maxlength="30">

Um Parameter an die Direktive zu binden, erhält sie eine Eigenschaft mit dem Namen des gewünschten Attributes. Zusätzlich ist die Eigenschaft mit Input zu dekorieren (Listing 7).

import { Directive, Input, forwardRef } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';

@Directive({
    selector: 'input[ort]',
    providers: [
        {
            provide: NG_VALIDATORS,
            useExisting: forwardRef(() => OrtValidatorDirective),
            multi: true
        }
    ]
})
export class OrtValidatorDirective implements Validator{

    @Input() ort: string;

    public validate(c: AbstractControl): any {

        var orte = this.ort.split(',');

        if (orte.indexOf(c.value) > -1) {
            return {};
        }

        return {
            ort: true
        }

    }

}

Der Dekorator Input veranlasst Angular, einen Attributwert über die Datenbindung entgegenzunehmen. Diesen nutzt das betrachtete Beispiel, um ein Array mit den erlaubten Orten zu erzeugen.

Asynchrone Validatoren

In einigen Fällen kann ein Validator nicht sofort feststellen, ob der Wert des übergebenen Feldes korrekt ist. Dies ist zum Beispiel der Fall, wenn er dazu eine serverseitige Routine anstoßen muss. Für solche Fälle sieht Angular asynchrone Validatoren vor. Diese antworten zunächst nur mit einem Promise, welches später mit einem Fehlerbeschreibungsobjekt aufzulösen ist. Ein Beispiel dafür findet sich in Listing 8.

import { Directive, Attribute, forwardRef } from '@angular/core';
import { Validator, AbstractControl, NG_ASYNC_VALIDATORS} from '@angular/forms';

@Directive({
    selector: 'input[ortAsync]', 
    providers: [
        {
            provide: NG_ASYNC_VALIDATORS,
            useExisting: forwardRef(() => OrtAsyncValidatorDirective),
            multi: true
        }
    ]
})
export class OrtAsyncValidatorDirective {

    public validate(c): Promise<any> {

        return new Promise<any>((resove) => {
            // Kommunikation mit Server simulieren
            setTimeout(() => {

                if (c.value == 'Graz'
                    || c.value == 'Hamburg') {
                    resove({});
                }

                resove({ ortAsync: true });
            }, 2000);
        });

    }

}

Per Definition führt der retournierte Promise den übergebenen Lambda-Ausdruck sofort aus. Dieser stößt zur Veranschaulichung ein Timeout, das stellvertretend für alle möglichen asynchronen Vorgänge steht, an. Wenn das Timeout eintritt, reicht er über die Methode resolve des Promise das benötigte Fehlerbeschreibungsobjekt zurück.

Damit Angular diese Direktive als asynchrone Validierungsdirektive erkennt, ist bei der Provider-Einstellung anstatt NG_VALIDATORS das Token NG_ASYNC_VALIDATORS zu nutzen. Im Markup gestaltet sich der Einsatz einer solchen Direktive wie gewohnt:

<input
    [(ngModel)]="von"
        name="von"
        class="form-control"
        required
        ort="Graz,München,Hamburg,Frankfurt,Zürich,Wien"
        ortAsync
        minlength="3"
        maxlength="30">

[…]

<div *ngIf="f?.controls?.von?.hasError('ort-async')">
  Derzeit gibt es keine Flüge für diesen Flughafen!
</div>

Zusätzlich pflegt Angular für asynchrone Validatoren eine Eigenschaft pending, die darüber Auskunft gibt, ob eine asynchrone Validierung noch im Gange ist:

<div *ngIf="f?.controls?.von?.pending">
    Validierung wird ausgeführt!
</div>

Fazit und Ausblick

Dank Two-Way-Binding gestaltet sich die Arbeit mit Formularen in Angular 2 sehr einfach. Die Direktive ngModel gleicht eine Eigenschaft der Komponente mit einem Formularfeld ab, und mit weiteren Attributen bestimmt die Anwendung, welche Felder zu validieren sind. Darüber hinaus lassen sich einfach eigene synchrone wie auch asynchrone Validierungsregeln erstellen.

Den aktuellen Zustand des Formulars spiegelt Angular über einen Objektgraph, welchen sich die Anwendung über ein Handle besorgen kann, wider. Seine Objekte und Eigenschaften können zum Einblenden von Fehlermeldungen herangezogen werden.

Neben den hier betrachteten Möglichkeiten zum Umgang mit Formularen bietet Angular 2 noch einige zusätzliche Aspekte. Beispielsweise können Anwendungen mit den sogenannten reaktiven Formularen den benötigten Objektgraph selbst aufbauen. Das bringt zwar mehr Aufwand mit sich, kommt aber auch mit mehr Freiheiten und ist die Basis für dynamische Formulare und Formulargeneratoren.

Geschrieben von
Manfred Steyer
Manfred Steyer
Manfred Steyer ist selbstständiger Trainer und Berater mit Fokus auf Angular 2. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten des populären JavaScript-Frameworks aus der Feder von Google. Web: www.softwarearchitekt.at
Kommentare
  1. YännuuH2017-03-20 12:41:37

    Vielen Dank für das super Tutorial!
    Möchte noch sagen, dass sich in Listing 8 ein kleiner Fehler eingeschlichen hat und zwar: wurde resove anstelle von resolve geschrieben.

    Liebe Grüsse

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.