Angular 2 user input form

How to create a custom component that will work as a native <input> ? I want my custom form control to support ngControl, ngForm, [(ngModel)].

As I understand it, I need to implement some interfaces so that my own form processing works the same as the native one.

Also, it seems that the ngForm directive only binds to the <input> , is that correct? How can I handle this?




Let me explain why I need this at all. I want to wrap several input elements so that they can work together as a single input. Is there any other way to handle this? One more time: I want to make this control the same as my own. Validation, ngForm, ngModel two-way binding and others.

ps: I am using Typescript.

+62
angular typescript
Jan 22 '16 at 14:24
source share
6 answers

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>&nbsp;|&nbsp;</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"):

+56
Jan 25 '16 at 17:40
source share

I do not understand why every example that I find on the Internet should be so complicated. Explaining the new concept, I believe that it is always better to have the simplest, working example. I redid this a bit:

HTML for an external form using a component that implements ngModel:

 EmailExternal=<input [(ngModel)]="email"> <inputfield [(ngModel)]="email"></inputfield> 

Autonomous component (there is no separate class "accessor" - maybe I will skip 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 just diverted all this stuff into an abstract class, which I now distribute with every component that I need to use ngModel. For me, this is a ton of top and standard code that I can handle.

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 a component is used 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> 
+46
Jun 13 '16 at 9:27
source share

Here is an example in this link for RC5 version: http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel

 import { Component, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; const noop = () => { }; export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomInputComponent), multi: true }; @Component({ selector: 'custom-input', template: `<div class="form-group"> <label> <ng-content></ng-content> <input [(ngModel)]="value" class="form-control" (blur)="onBlur()" > </label> </div>`, providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR] }) export class CustomInputComponent implements ControlValueAccessor { //The internal data model private innerValue: any = ''; //Placeholders for the callbacks which are later providesd //by the Control Value Accessor private onTouchedCallback: () => void = noop; private onChangeCallback: (_: any) => void = noop; //get accessor get value(): any { return this.innerValue; }; //set accessor including call the onchange callback set value(v: any) { if (v !== this.innerValue) { this.innerValue = v; this.onChangeCallback(v); } } //Set touched on blur onBlur() { this.onTouchedCallback(); } //From ControlValueAccessor interface writeValue(value: any) { if (value !== this.innerValue) { this.innerValue = value; } } //From ControlValueAccessor interface registerOnChange(fn: any) { this.onChangeCallback = fn; } //From ControlValueAccessor interface registerOnTouched(fn: any) { this.onTouchedCallback = fn; } } 

Then we can use this custom control as follows:

 <form> <custom-input name="someValue" [(ngModel)]="dataModel"> Enter data: </custom-input> </form> 
+11
Aug 22 '16 at 9:08 on
source share

Thierry's example is helpful. Here is the import that is needed to run the TagsValueAccessor function ...

 import {Directive, Provider} from 'angular2/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR } from 'angular2/common'; import {CONST_EXPR} from 'angular2/src/facade/lang'; import {forwardRef} from 'angular2/src/core/di'; 
+5
Mar 29 '16 at 13:11
source share

You can also solve this problem with the @ViewChild directive. This gives parental full access to all the member variables and functions of the injected child.

See: How to access the fields of input components of the form

0
Dec 12 '16 at 9:09
source share

Why create a new access attribute when you can use the internal ngModel. Whenever you create a custom component that has the [ngModel] input in it, we already create an instance of ControlValueAccessor. And that we need an accessor.

template:

 <div class="form-group" [ngClass]="{'has-error' : hasError}"> <div><label>{{label}}</label></div> <input type="text" [placeholder]="placeholder" ngModel [ngClass]="{invalid: (invalid | async)}" [id]="identifier" name="{{name}}-input" /> </div> 

component:

 export class MyInputComponent { @ViewChild(NgModel) innerNgModel: NgModel; constructor(ngModel: NgModel) { //First set the valueAccessor of the outerNgModel this.outerNgModel.valueAccessor = this.innerNgModel.valueAccessor; //Set the innerNgModel to the outerNgModel //This will copy all properties like validators, change-events etc. this.innerNgModel = this.outerNgModel; } } 

Use as:

 <my-input class="col-sm-6" label="First Name" name="firstname" [(ngModel)]="user.name" required minlength="5" maxlength="20"></my-input> 
0
Feb 21 '17 at 12:37
source share



All Articles