Angular 2自定义表单input
我怎样才能创build自定义组件,就像本地<input>
标签一样工作? 我想让我的自定义窗体控件能够支持ngControl,ngForm,[(ngModel)]。
据我所知,我需要实现一些接口,使自己的窗体控制工作就像本地一样。
此外,似乎像ngForm指令只绑定<input>
标记,是这样吗? 我该如何处理?
让我解释一下为什么我需要这个。 我想包装几个input元素,使他们能够作为一个单一的input工作在一起。 有没有其他的方式来处理呢? 还有一次:我想把这个控件做成像本地控件一样。 validation,ngForm,ngModel双向绑定等。
ps:我使用Typescript。
实际上,有两件事要实现:
- 提供表单组件逻辑的组件。 它不是一个input,因为它将由
ngModel
本身提供 - 自定义的
ControlValueAccessor
,将实现此组件和ngModel
/ngControl
之间的桥梁
我们来看一个例子。 我想实现一个pipe理公司标签列表的组件。 该组件将允许添加和删除标签。 我想添加一个validation,以确保标签列表不是空的。 我将在我的组件中定义它,如下所述:
(...) 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] }); } }
TagsComponent
组件定义了添加和移除tags
列表中元素的逻辑。
@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 = ''; } }
正如你所看到的,这个组件中没有input,而是一个setValue
(这里的名字不重要)。 我们稍后使用它来将ngModel
的值提供给组件。 这个组件定义了一个事件来通知组件(标签列表)的状态何时被更新。
现在我们来实现这个组件和ngModel
/ ngControl
之间的链接。 这对应于实现ControlValueAccessor
接口的指令。 必须针对NG_VALUE_ACCESSOR
标记为此值访问器定义提供者(不要忘记使用forwardRef
因为指令是在之后定义的)。
该指令将在主机的tagsChange
事件(即指令所附的组件,即TagsComponent
)上附加一个事件监听器。 事件发生时会调用onChange
方法。 此方法对应于Angular2注册的方法。 这样它会知道更改和更新相应的表单控件。
当ngForm
的值绑定更新时, writeValue
被调用。 注入附件(即TagsComponent)后,我们可以调用它来传递这个值(参见前面的setValue
方法)。
不要忘记在指令的绑定中提供CUSTOM_VALUE_ACCESSOR
。
这是自定义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; } }
这样,当我删除公司的所有tags
时, companyForm.controls.tags
控件的valid
属性会自动变为false
。
看到这篇文章(“NgModel兼容组件”部分)的更多细节:
我不明白为什么我在互联网上find的每一个例子都非常复杂。 在解释一个新的概念时,我认为最好有最简单的工作例子。 我已经把它细化了一下:
使用组件实现ngModel的外部表单的HTML:
EmailExternal=<input [(ngModel)]="email"> <inputfield [(ngModel)]="email"></inputfield>
自包含的组件(没有单独的“访问者”类 – 也许我错过了这一点):
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; } }
实际上,我只是将所有这些东西都抽象成了一个抽象类,现在我使用ngModel来扩展每个组件。 对我来说,这是一大堆开销和样板代码,我可以不用。
编辑:这里是:
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 }; }
这是一个使用它的组件:(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>
在RC5版本的链接中有一个例子: 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; } }
然后我们可以使用这个自定义控件如下:
<form> <custom-input name="someValue" [(ngModel)]="dataModel"> Enter data: </custom-input> </form>
蒂埃里的例子是有帮助的。 这里是TagsValueAccessor运行所需的导入…
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';
你也可以用@ViewChild指令解决这个问题。 这使得父母可以完全访问注入的孩子的所有成员variables和函数。
请参阅: 如何访问注入表单组件的input字段
当你可以使用内部的ngModel的时候,为什么要创build一个新的值存取器。 每当你创build一个自定义的组件,其中有一个input[ngModel],我们已经实例化一个ControlValueAccessor。 这是我们需要的访问器。
模板:
<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>
零件:
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; } }
用于:
<my-input class="col-sm-6" label="First Name" name="firstname" [(ngModel)]="user.name" required minlength="5" maxlength="20"></my-input>