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);
}
}