Parser Und Formatter In Angular 2

Parser Und Formatter In Angular 2

AngularJS 1.x erlaubt mit seinem Parser-Konzept das Verarbeiten von Benutzereingaben, bevor ng-model sie über die Datenbindung ans Model zurückschreibt. Analog dazu können Formatter gebundene Daten aus dem Model formatieren, bevor sie in einem Eingabefeld aufscheinen.

In Angular 2 gibt es solch ein Konzept nicht - zumindest nicht auf den ersten Blick. Allerdings findet man hier das Konzept der ValueAccessor. Dabei handelt es sich um Klassen, die für die Synchronisation zwischen Steuerelementen und dem Model zuständig sind. Da unterschiedliche Steuerelemente, wie Checkboxes oder Eingabefelder, hierbei unterschiedlich zu nutzen sind, gibt es auch verschiedene ValueAccessor-Implementierungen.

Für eigene Implementierungen bietet sich u. a. das Interface. ControlValueAccessor an. Es definiert die Methoden registerOnChange sowie registerOnTouched, welche von Angular Callbacks übergeben bekommen. Diese nutzt der Value-Accessor um geänderte Daten zum Controller zurückzusenden. Sie sind nach Änderungen im jeweiligen Steuerelement oder beim Verlassen des Steuerelements anzustoßen. Daneben gibt das Interface die Methode writeValue vor. Sie bekommt von Angular Werte aus dem Model, welche es in das Steuerelement zu schreiben gilt.

Das nachfolgende Beispiel demonstriert, wie Entwicklungs-Teams damit die gebundenen Daten im Rahmen der Datenbindung modifizieren können. Es wandelt ein Datum, das im Model als ISO-String vorliegt, in ein deutsches Datum um und schreibt Modifikationen an diesem Datum in Form eines ISO-Strings ins Model zurück.

Um das Zurückschreiben ins Model zu beeinflussen, richtet das betrachtete Beispiel Event-Handler für das input- und das blur-Event des Host-Steuerelements ein. Beim Host-Steuerelement handelt es sich beispielsweise um ein Eingabefeld (<input>) oder eine Textarea. Diese definiert es über die Eigenschaft host des Directive-Dekorators. Der Event-Handler für input modifiziert die Eingaben durch Aufruf des zuvor besprochenen Callbacks onChange.

Zum Beeinflussen der im Steuerelement präsentierten Daten überschreibt das Beispiel die Methode writeValue. Sie kümmert sich um die Formatierung und delegiert anschließend an die Basis-Implementierung von writeValue, welche ins Steuerelement schreibt, weiter.

Über die Eigenschaft selector gibt die Implementierung bekannt, für welche Elemente sie zu nutzen ist. Der Wert input[date] adressiert dabei input-Elemente mit einem date-Attribut, beispielsweise <input date [(ng-model)]="datum">.

Damit Angular 2 die Direktive tatsächlich als ValueAccessor nutzt, ist ein Provider einzurichten, der sie ans Token NG_VALUE_ACCESSOR bindet. Zur Laufzeit ruft Angular 2 sämtliche Elemente, die an dieses Token gebunden wurden, ab und nutzt sie zur Datenbindung beim jeweiligen Element. Die beim Definieren des Providers hinterlegte Angabe multi: true sagt aus, dass mehrere Elemente an NG_VALUE_ACCESSOR gebunden sein dürfen. Die Indirektion über forwardRef löst das Henne-Ei-Problem, das durch die gegenseitige Referenzierung zwischen dem Provider und dem ValueAccessor entsteht.

import {Directive, Renderer, ElementRef, Self, forwardRef, provide} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

@Directive({
    selector: '[mydate]',
    host: {'(input)': 'input($event.target.value)', '(blur)': 'blur()'},
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => DateValueAccessor),
        multi: true}]
})
export class DateValueAccessor implements ControlValueAccessor {

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

    constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}

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

    blur() {
        this.onTouched();
    }

    // Parser: View --> Ctrl
    input(value) {

        // Write back to model
        if (value) {
            value = value.split(/\./);
            value = value[2] + "-" + value[1] + "-" + value[0];
        }

        this.onChange(value);
    }

    // Formatter: Ctrl --> View
    writeValue(value: any): void {

        // Write to view
        if (value) {
            var date = new Date(value);

            value =
                date.getDate() + "."
                    + (date.getMonth()+1) + "."
                    + date.getFullYear();
        }

        var normalizedValue = (value) ? value : '';
        this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);

    }

}