Eigene Formular-Steuerelemente Für Angular 2 Schreiben

Eigene Formular-Steuerelemente Für Angular 2 Schreiben

Update im Jänner 2017: Dieser Beitrag wurde für die finale API von Angular 2 aktualisiert.

Sollen eigene Angular 2-Komponenten mit deklarativen (template-driven) und imperativen Formularen zusammenspielen, ist das Interface ControlValueAccessor zu implementieren. Die von diesem Interface definierten Methoden erlauben ein Abgleichen der Komponente mit dem Objektgraphen, über den Angular 2 ein Formular beschreibt.

Solche Steuerelemente arbeiten - wie input-Elemente - mit ngModel bzw. ngControl zusammen. Zur Veranschaulichung nutzt das nachfolgende Beispiel eine benutzerdefinierte Komponente date-control, die sich an die Eigenschaft mit ngModel an die Eigenschaft date bindet:

<!-- Deklaratives (template-driven) Forms-Handling 
<date-control [(ngModel)]="date"></date-control>

Alternativ dazu kann sich solch eine Komponente jedoch auch über das imperative Forms-Handling an ein vordefiniertes Control-Objekt binden. Das nachfolgende Beispiel zeigt dies, indem es das date-control mit formControlName an das FormControl-Objekt mit dem Namen date bindet. Dieses Control-Objekt erwartet Angular in der über formGroup festgelegten FormGroup.

<!-- Imperatives Forms-Handling -->
<form [formGroup]="filter">   
    <date-control formControlName="date"></date-control>
    [...]
</form>

Die FormGroup sowie das FormControl sind dabei über die Komponente bereitzustellen:

@Component({
    selector: 'flight-search',  
    template: require('./flight-search.component.html'),
    directives: [DateControlComponent]
})
export class FlightSearchImpComponent {

    public filter: FormGroup;

    constructor(private fb: FormBuilder) {

        this.filter = fb.group({
           date: ['2016-05-01']
        });
    }

    [...]
}

Dieser Beitrag beschreibt die nötigen Schritte zur Implementierung von ControlValueAccessor. Das gesamte Beispiel findet sich hier.

ControlValueAccessor

Das von Angular 2 bereitgestellte Interface ControlValueAccessor bietet drei Methoden zum Abgleich des Zustands eines Steuerelement mit dem von Angular für das Formular eingerichteten Objektgraphen.

//
// From the Angular2-Sources
//
export interface ControlValueAccessor {
    writeValue(obj: any): void;
    registerOnChange(fn: any): void;
    registerOnTouched(fn: any): void;
}

Um einen Wert ins Steuerelement zu schreiben, nutzt Angular die Methode writeValue. Da Angular jedoch auch über Änderungen am Laufenden bleiben muss, registriert das SPA-Flagschiff mit registerOnChange und registerOnTouched jeweils einen Callback. Ersteren ruft das Steuerelement per Definition auf, wenn der Benutzer den Wert ändert. Letzteres signalisiert, dass das Feld zumindest den Fokus hatte.

ControlValueAccessor implementieren

Das nachfolgende Beispiel demonstriert die Realisierung des Interfaces ControlValueAccessor. Es handelt sich dabei um eine einfache Komponente zum Bearbeiten von Datumswerten. Die Methode splitDate nimmt ein Datum entgegen und zerlegt es in seine Bestandteile, die es anschließend im (hier nicht abgebildeten) Template zum Editieren anbietet. Den umgekehrten Weg beschreitet die Methode apply, indem sie diese einzelnen Bestandteile wieder zum einem Datum zusammenfügt.

import { Component } from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';

@Component({
    selector: 'date-control',
    template: require('./date-control.component.html')
})
export class DateControlComponent 
                    implements ControlValueAccessor {

    day: number;
    month: number;
    year: number;
    hour: number;
    minute: number;

    constructor(private c: NgControl) {
        c.valueAccessor = this;
    }

    writeValue(value: any) {
        this.splitDate(value);
    }

    onChange = (_) => {};
    onTouched = () => {};

    registerOnChange(fn): void { this.onChange = fn; }
    registerOnTouched(fn): void { this.onTouched = fn; }

    splitDate(dateString) {
      var date = new Date(dateString); 

      this.day = date.getDate();
      this.month = date.getMonth() + 1;
      this.year = date.getFullYear();
      this.hour = date.getHours();
      this.minute = date.getMinutes();
    }

    apply() {

        var date = new Date();
        date.setDate(this.day);
        date.setMonth(this.month - 1);
        date.setFullYear(this.year);
        date.setHours(this.hour);
        date.setMinutes(this.minute);
        date.setSeconds(0);
        date.setMilliseconds(0);

        this.onChange(date.toISOString());
        this.onTouched();
    }

}

Damit diese Komponente mit dem Forms-Handling von Angular 2 zusammenspielen kann, implementiert sie das Interface ControlValueAccessor. Zusätzlich lässt sie sich die aktuelle Instanz von NgControl injizieren. Diese Instanz repräsentiert die Komponente im von Angular für ein Formular erzeugten Objektgraphen. Über die Eigenschaft valueAccessor gibt die Komponente bekannt, dass sie selbst als ihr eigener ControlValueAccessor fungiert.

Die Implementierung von writeValue nimmt einen neuen Wert vom Framework entgegen und delegiert ihn an splitDate. Die Implementierungen von registerOnChange und registerOnTouched nehmen hingegen den von Angular übergebenen Callback entgegen und hinterlegen diese in den Member-Variablen onChange bzw. onTouched.

Standardmäßig verweisen diese Member auf Funktionen, die keine Aufgaben erfüllen. Auf diese Weise ist gewährleistet, dass sie zu jedem Zeitpunkt auf eine gültige Funktion verweisen. Somit muss sie ein Aufrufer nicht gegen null bzw. undefined prüfen.

Nach dem Bearbeiten des Datums ruft das Template die Methode apply auf. Sie fügt die einzelnen Bestandteile des Datums zu einem Datum zusammen und übergibt es durch Aufruf von onChange an Angular. Zusätzlich bringt es der Vollständigkeit halber die Methode onTouched zur Ausführung.