Updated in January 2017: This post has been revised for the final Angular 2 API.
To make custom Angular 2 components work with both declarative (template-driven) and imperative forms, you need to implement the ControlValueAccessor interface. The methods this interface defines let you synchronize your component with the object graph Angular 2 uses to represent a form.
Such controls collaborate with ngModel and ngControl, just like native input elements do. To illustrate, the next example uses a custom date-control component that binds to the date property via ngModel:
<!-- Deklaratives (template-driven) Forms-Handling
<date-control [(ngModel)]="date"></date-control>
Alternatively, the same component can hook into a predefined Control object through the imperative forms API. The following snippet shows this approach by binding date-control with formControlName to an FormControl named date. Angular expects this control object to live inside the FormGroup set up through formGroup.
<!-- Imperatives Forms-Handling -->
<form [formGroup]="filter">
<date-control formControlName="date"></date-control>
[...]
</form>
The FormGroup and the FormControl need to be provided by the surrounding 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 walks through the steps needed to implement ControlValueAccessor. The complete example is available here.
Understanding ControlValueAccessor
The ControlValueAccessor interface, shipped with Angular 2, exposes three methods for keeping a control in sync with the object graph Angular builds for a form.
//
// From the Angular2-Sources
//
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
}
Angular calls writeValue to push a value into the control. To stay informed about changes, the framework registers two callbacks via registerOnChange and registerOnTouched. By convention, the control invokes the first callback whenever the user alters the value; the second signals that the field has at least received focus.
Implementing ControlValueAccessor
The example below demonstrates how to implement the ControlValueAccessor interface. It shows a simple component for editing date values. The splitDate method takes a date and breaks it into its parts, which the (omitted) template then renders for editing. The apply method performs the reverse operation by reassembling those parts back into a date.
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 let this component participate in Angular 2's forms handling, it implements the ControlValueAccessor interface. In addition, it injects the current NgControl instance. That instance represents the component inside the object graph Angular creates for a form. Through the valueAccessor property, the component declares that it acts as its own ControlValueAccessor.
The implementation of writeValue accepts a new value from the framework and forwards it to splitDate. The implementations of registerOnChange and registerOnTouched, meanwhile, store the callbacks supplied by Angular in the member variables onChange and onTouched.
By default, these members point to no-op functions. This ensures they always reference a valid function, so callers do not need to check against null or undefined.
Once the user finishes editing the date, the template invokes the apply method. It combines the individual date parts back into a date and hands it to Angular by calling onChange. For completeness, it also triggers the onTouched method.
