AngularJS 1.x introduced the concept of parsers, which allow user input to be processed before ng-model writes it back to the model through data binding. Similarly, formatters can take model-bound data and format it before it appears inside an input field.

Angular 2 does not offer this exact mechanism—at least not at first glance. However, it introduces the notion of ValueAccessor classes, which are responsible for keeping controls and the model in sync. Because different controls, such as checkboxes or text inputs, need different handling, multiple ValueAccessor implementations exist.

For custom implementations, the ControlValueAccessor interface is a key starting point. It declares the methods registerOnChange and registerOnTouched, which receive callbacks from Angular. The value accessor uses these callbacks to report updated data back to the controller, triggering them on changes within the control or when the control loses focus. The interface also defines writeValue, which Angular invokes with model values that need to be written into the control.

The following example demonstrates how development teams can use this mechanism to alter bound data during the data binding process. It takes a date stored in the model as an ISO string, converts it into a German date format for display, and sends any modifications back to the model as an ISO string.

To influence the write-back to the model, the example registers event handlers for the input and blur events on the host control. The host control could be an input element (<input>) or a textarea, as specified through the host property of the Directive decorator. The input event handler processes user input by invoking the previously mentioned onChange callback.

To control what appears in the control, the example overrides the writeValue method. It handles the formatting logic and then delegates to the base implementation of writeValue, which performs the actual write into the control.

Through the selector property, the implementation declares which elements it applies to. The value input[date] targets input elements that carry a date attribute, such as <input date [(ng-model)]="datum">.

For Angular 2 to recognize the directive as a ValueAccessor, a provider must be configured to bind it to the token NG_VALUE_ACCESSOR. At runtime, Angular 2 retrieves all elements bound to this token and uses them for data binding on the respective element. The multi: true flag in the provider definition indicates that multiple elements may be associated with NG_VALUE_ACCESSOR. The indirection via forwardRef resolves the chicken-and-egg problem arising from the mutual reference between the provider and the ValueAccessor.

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

    }

}