There are actually two things to implement:
- A component that provides the logic for your form component. This is not an input, as it will be provided by
ngModel itself - A custom
ControlValueAccessor that implements a bridge between this component and ngModel / ngControl
Take a sample. I want to implement a component that manages a tag list for a company. The component will allow you to add and remove tags. I want to add a check so that the tag list is not empty. I will define it in my component as described below:
(...) import {TagsComponent} from './app.tags.ngform'; import {TagsValueAccessor} from './app.tags.ngform.accessor'; function notEmpty(control) { if(control.value == null || control.value.length===0) { return { notEmpty: true } } return null; } @Component({ selector: 'company-details', directives: [ FormFieldComponent, TagsComponent, TagsValueAccessor ], template: ` <form [ngFormModel]="companyForm"> Name: <input [(ngModel)]="company.name" [ngFormControl]="companyForm.controls.name"/> Tags: <tags [(ngModel)]="company.tags" [ngFormControl]="companyForm.controls.tags"></tags> </form> ` }) export class DetailsComponent implements OnInit { constructor(_builder:FormBuilder) { this.company = new Company('companyid', 'some name', [ 'tag1', 'tag2' ]); this.companyForm = _builder.group({ name: ['', Validators.required], tags: ['', notEmpty] }); } }
The TagsComponent component defines the logic for adding and removing elements in the tags list.
@Component({ selector: 'tags', template: ` <div *ngIf="tags"> <span *ngFor="#tag of tags" style="font-size:14px" class="label label-default" (click)="removeTag(tag)"> {{label}} <span class="glyphicon glyphicon-remove" aria- hidden="true"></span> </span> <span> | </span> <span style="display:inline-block;"> <input [(ngModel)]="tagToAdd" style="width: 50px; font-size: 14px;" class="custom"/> <em class="glyphicon glyphicon-ok" aria-hidden="true" (click)="addTag(tagToAdd)"></em> </span> </div> ` }) export class TagsComponent { @Output() tagsChange: EventEmitter; constructor() { this.tagsChange = new EventEmitter(); } setValue(value) { this.tags = value; } removeLabel(tag:string) { var index = this.tags.indexOf(tag, 0); if (index != undefined) { this.tags.splice(index, 1); this.tagsChange.emit(this.tags); } } addLabel(label:string) { this.tags.push(this.tagToAdd); this.tagsChange.emit(this.tags); this.tagToAdd = ''; } }
As you can see, there is no input in this component, but <<28> (the name is not important here). We will use it later to provide the value from ngModel to the component. This component defines an event for notification when the state of the component (tag list) is updated.
Now we implement the connection between this component and ngModel / ngControl . This corresponds to a directive implementing the ControlValueAccessor interface. A provider must be defined for this token access attribute NG_VALUE_ACCESSOR (remember to use forwardRef , since the directive is defined after).
The directive will attach an event listener to the tagsChange event of the node (that is, to the component to which the directive is attached, i.e. TagsComponent ). When an event occurs, the onChange method is onChange . This method corresponds to the one registered by Angular2. Thus, he will be aware of changes and updates corresponding to the corresponding form control.
Called writeValue when updating a value in ngForm . After you added the component attached (i.e. TagsComponent), we can call it to pass this value (see the previous setValue method).
Remember to specify CUSTOM_VALUE_ACCESSOR in the directive bindings.
Here is the complete code for custom ControlValueAccessor :
import {TagsComponent} from './app.tags.ngform'; const CUSTOM_VALUE_ACCESSOR = CONST_EXPR(new Provider( NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => TagsValueAccessor), multi: true})); @Directive({ selector: 'tags', host: {'(tagsChange)': 'onChange($event)'}, providers: [CUSTOM_VALUE_ACCESSOR] }) export class TagsValueAccessor implements ControlValueAccessor { onChange = (_) => {}; onTouched = () => {}; constructor(private host: TagsComponent) { } writeValue(value: any): void { this.host.setValue(value); } registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } }
That way, when I remove all company tags , the valid attribute of the companyForm.controls.tags control automatically becomes false .
See this article for more details (section "NgModel compatible component"):