How can I create custom component which would work just like native <input>
tag? I want to make my custom form control be able to support ngControl, ngForm, [(ngModel)].
As I understand, I need to implement some interfaces to make my own form control work just like native one.
Also, seems like ngForm directive binds only for <input>
tag, is this right? How can i deal with that?
Let me explain why I need this at all. I want to wrap several input elements to make them able to work together as one single input. Is there other way to deal with that? One more time: I want to make this control just like native one. Validation, ngForm, ngModel two way binding and other.
ps: I use Typescript.
What are form controls in Angular? In Angular, form controls are classes that can hold both the data values and the validation information of any form element. Every form input you have in a reactive form should be bound by a form control. These are the basic units that make up reactive forms.
FormsModule. Exports the required providers and directives for template-driven forms, making them available for import by NgModules that import this module. ReactiveFormsModule. Exports the required infrastructure and directives for reactive forms, making them available for import by NgModules that import this module.
ValidatorFnlinkA function that receives a control and synchronously returns a map of validation errors if present, otherwise null. interface ValidatorFn { (control: AbstractControl<any, any>): ValidationErrors | null }
The FormBuilder provides syntactic sugar that shortens creating instances of a FormControl , FormGroup , or FormArray . It reduces the amount of boilerplate needed to build complex forms.
I don't understand why every example I find on the internet has to be so complicated. When explaining a new concept, I think it's always best to have the most simple, working example possible. I've distilled it down a little bit:
HTML for external form using component implementing ngModel:
EmailExternal=<input [(ngModel)]="email"> <inputfield [(ngModel)]="email"></inputfield>
Self-contained component (no separate 'accessor' class - maybe I'm missing the point):
import {Component, Provider, forwardRef, Input} from "@angular/core"; import {ControlValueAccessor, NG_VALUE_ACCESSOR, CORE_DIRECTIVES} from "@angular/common"; const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = new Provider( NG_VALUE_ACCESSOR, { useExisting: forwardRef(() => InputField), multi: true }); @Component({ selector : 'inputfield', template: `<input [(ngModel)]="value">`, directives: [CORE_DIRECTIVES], providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR] }) export class InputField implements ControlValueAccessor { private _value: any = ''; get value(): any { return this._value; }; set value(v: any) { if (v !== this._value) { this._value = v; this.onChange(v); } } writeValue(value: any) { this._value = value; this.onChange(value); } onChange = (_) => {}; onTouched = () => {}; registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } }
In fact, I've just abstracted all of this stuff to an abstract class which I now extend with every component I need to use ngModel. For me this is a ton of overhead and boilerplate code which I can do without.
Edit: Here it is:
import { forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; export abstract class AbstractValueAccessor implements ControlValueAccessor { _value: any = ''; get value(): any { return this._value; }; set value(v: any) { if (v !== this._value) { this._value = v; this.onChange(v); } } writeValue(value: any) { this._value = value; // warning: comment below if only want to emit on user intervention this.onChange(value); } onChange = (_) => {}; onTouched = () => {}; registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } } export function MakeProvider(type : any){ return { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => type), multi: true }; }
Here's a component that uses it: (TS):
import {Component, Input} from "@angular/core"; import {CORE_DIRECTIVES} from "@angular/common"; import {AbstractValueAccessor, MakeProvider} from "../abstractValueAcessor"; @Component({ selector : 'inputfield', template: require('./genericinput.component.ng2.html'), directives: [CORE_DIRECTIVES], providers: [MakeProvider(InputField)] }) export class InputField extends AbstractValueAccessor { @Input('displaytext') displaytext: string; @Input('placeholder') placeholder: string; }
HTML:
<div class="form-group"> <label class="control-label" >{{displaytext}}</label> <input [(ngModel)]="value" type="text" placeholder="{{placeholder}}" class="form-control input-md"> </div>
In fact, there are two things to implement:
ngModel
itselfControlValueAccessor
that will implement the bridge between this component and ngModel
/ ngControl
Let's take a sample. I want to implement a component that manages a list of tags for a company. The component will allow to add and remove tags. I want to add a validation to ensure that the tags list isn't 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 to add and remove 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 !== -1) { 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 a setValue
one (the name isn't important here). We use it later to provide the value from the ngModel
to the component. This component defines an event to notify when the state of the component (the tags list) is updated.
Let's implement now the link between this component and ngModel
/ ngControl
. This corresponds to a directive that implements the ControlValueAccessor
interface. A provider must be defined for this value accessor against the NG_VALUE_ACCESSOR
token (don't forget to use forwardRef
since the directive is defined after).
The directive will attach an event listener on the tagsChange
event of the host (i.e. the component the directive is attached on, i.e. the TagsComponent
). The onChange
method will be called when the event occurs. This method corresponds to the one registered by Angular2. This way it will be aware of changes and updates accordingly the associated form control.
The writeValue
is called when the value bound in the ngForm
is updated. After having injected the component attached on (i.e. TagsComponent), we will be able to call it to pass this value (see the previous setValue
method).
Don't forget to provide the CUSTOM_VALUE_ACCESSOR
in the bindings of the directive.
Here is the complete code of the 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; } }
This way when I remove all the tags
of the company, the valid
attribute of the companyForm.controls.tags
control becomes false
automatically.
See this article (section "NgModel-compatible component") for more details:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With