Writing Custom Form Controls For Angular 2 [EN]

Writing Custom Form Controls For Angular 2 [EN]

Update in January 2017: This article has been updated for the final API of Angular 2.x.

If you want your components to work with declarative (template-driven) and imperative forms, you have to implement the interface ControlValueAccessor. The methods defined by this interface permit Angular 2 to read and set the state of the control in question.

Such controls can be used with ngModel or ngControl. To illustrate this, the following example uses a custom date-control that binds a variable date with ngModel:

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

Alternatively, such a component can use imperative forms to bind to a predefined Control-object. The following example illustrates this by binding the date-control with formControlName to the FormControl with the name date. Angular expects this Control-object in the FormGroup that has been specified with formGroup:

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

The ControlGroup and the Control are provided via the component:

@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']
        });
    }

    [...]
}

This article describes the steps necessary to implement ControlValueAccessor. The total sample can be found here.

ControlValueAccessor

The interface ControlValueAccessor provides three methods for synchronizing the state of a control with the object graph Angular uses to represent a form:

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

To write a value to the control, Angular uses the method writeValue. Since Angular must know about changes to the date, the SPA-flagship uses registerOnChange and registerOnTouched to register callbacks. The control has to call the first one, when the users changes the state. The latter one indicates that the field at least had the focus.

Implementing ControlValueAccessor

The following example demonstrates the implementation of the interface ControlValueAccessor. For this, it shows a simple component for editing dates. The method splitDate takes a date and breaks it down into its parts, which are offered for editing by the (here not shown) template. The method apply is putting those together to a date again.

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

}

To be able to interact with the Forms-Handling of Angular 2, the component implements the interface ControlValueAccessor. In addition, it gets the current NgControl by the means of Dependency Injection. Angular uses this instance to represent the control within the object graph that has been created for the form. It sets the property ValueAccessor to the component itself. This means, that the component is it's own ValueAccessor.

The implementation of writeValue takes a new value from the framework and delegates it to splitDate. The implementations of registerOnChange and registerOnTouched however take callbacks from Angular and puts them into onChange and onTouched.

After the date has been changed, the template invokes the method apply. It adds the individual parts of the date together and passes it to Angular using onChange. In addition, for the sake of completeness, it executes the calback onTouched.