Angular 2 自定义表单输入
Posted
技术标签:
【中文标题】Angular 2 自定义表单输入【英文标题】:Angular 2 custom form input 【发布时间】:2016-04-29 04:28:29 【问题描述】:如何创建与原生 <input>
标签一样的自定义组件?我想让我的自定义表单控件能够支持 ngControl、ngForm、[(ngModel)]。
据我了解,我需要实现一些接口以使我自己的表单控件像原生控件一样工作。
另外,ngForm 指令似乎只绑定<input>
标签,对吗?我该如何处理?
让我解释一下为什么我需要这个。我想包装几个输入元素,使它们能够作为一个输入一起工作。有没有其他方法来处理它? 再来一次:我想让这个控件就像原生控件一样。验证、ngForm、ngModel 双向绑定等。
ps:我使用 Typescript。
【问题讨论】:
关于当前 Angular 版本的大多数答案都已过时。看看***.com/a/41353306/2176962 【参考方案1】:我不明白为什么我在互联网上找到的每个示例都必须如此复杂。在解释一个新概念时,我认为最好有最简单、最有效的例子。我把它提炼了一点:
使用实现 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>
【讨论】:
有趣的是,自 RC2 以来,接受的答案似乎已经停止工作,我尝试了这种方法并且它有效,但不知道为什么。 @3urdoch 当然,一秒 要使其与新的@angular/forms
一起使用,只需更新导入:import ControlValueAccessor, NG_VALUE_ACCESSOR from '@angular/forms'
Provider() 在 Angular2 Final 中不受支持。相反,让 MakeProvider() return provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => type), multi: true ;
您不再需要导入 CORE_DIRECTIVES
并将它们添加到 @Component
中,因为它们是自 Angular2 最终版以来默认提供的。但是,根据我的 IDE,“派生类的构造函数必须包含 'super' 调用。”,所以我必须将 super();
添加到组件的构造函数中。【参考方案2】:
其实有两件事要实现:
提供表单组件逻辑的组件。它不需要输入,因为它将由ngModel
自己提供
一个自定义的ControlValueAccessor
将实现此组件和ngModel
/ ngControl
之间的桥梁
让我们取样。我想实现一个管理公司标签列表的组件。该组件将允许添加和删除标签。我想添加一个验证以确保标签列表不为空。我将在我的组件中定义它,如下所述:
(...)
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 !== -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 = '';
如您所见,此组件中没有输入,而是一个 setValue
(名称在这里并不重要)。我们稍后使用它来将来自ngModel
的值提供给组件。该组件定义了一个事件,当组件的状态(标签列表)更新时进行通知。
现在让我们实现这个组件和ngModel
/ngControl
之间的链接。这对应于实现ControlValueAccessor
接口的指令。必须针对 NG_VALUE_ACCESSOR
令牌为此值访问器定义提供程序(不要忘记使用 forwardRef
,因为该指令是在之后定义的)。
该指令将在主机的tagsChange
事件上附加一个事件侦听器(即附加该指令的组件,即TagsComponent
)。事件发生时将调用onChange
方法。此方法对应于 Angular2 注册的方法。这样它就会知道相关的表单控件的变化并相应地更新。
writeValue
在ngForm
中绑定的值更新时被调用。在注入附加的组件(即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 兼容组件”部分):
http://restlet.com/blog/2016/02/17/implementing-angular2-forms-beyond-basics-part-2/【讨论】:
谢谢!你真棒!你怎么想 - 这种方式真的好吗?我的意思是:不要使用输入元素并制作自己的控件,例如:<textfield>
、<dropdown>
?这是“棱角分明”的方式吗?
我想说如果你想在表单中实现自己的字段(自定义),请使用这种方法。否则使用原生 HTML 元素。也就是说,如果您想模块化显示输入/文本区域/选择的方式(例如使用 Bootstrap3),您可以利用 ng-content。看到这个答案:***.com/questions/34950950/…
上面缺少代码并且有一些差异,比如'removeLabel'而不是'removeLabel'。有关完整的工作示例,请参阅here。感谢蒂埃里把最初的例子放在那里!
找到它,从@angular/forms 导入而不是@angular/common 并且它可以工作。从“@angular/forms”导入 NG_VALUE_ACCESSOR, ControlValueAccessor;
this 链接也应该有帮助..【参考方案3】:
此链接中有一个 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>
【讨论】:
虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。【参考方案4】:蒂埃里的例子很有帮助。以下是 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';
【讨论】:
【参考方案5】:我编写了一个库来帮助减少这种情况下的一些样板:s-ng-utils
。其他一些答案给出了包装 single 表单控件的示例。使用s-ng-utils
可以非常简单地使用WrappedFormControlSuperclass
:
@Component(
template: `
<!-- any fancy wrapping you want in the template -->
<input [formControl]="formControl">
`,
providers: [provideValueAccessor(StringComponent)],
)
class StringComponent extends WrappedFormControlSuperclass<string>
// This looks unnecessary, but is required for Angular to provide `Injector`
constructor(injector: Injector)
super(injector);
在您的帖子中,您提到您希望将多个表单控件包装到一个组件中。这是使用FormControlSuperclass
执行此操作的完整示例。
import Component, Injector from "@angular/core";
import FormControlSuperclass, provideValueAccessor from "s-ng-utils";
interface Location
city: string;
country: string;
@Component(
selector: "app-location",
template: `
City:
<input
[ngModel]="location.city"
(ngModelChange)="modifyLocation('city', $event)"
/>
Country:
<input
[ngModel]="location.country"
(ngModelChange)="modifyLocation('country', $event)"
/>
`,
providers: [provideValueAccessor(LocationComponent)],
)
export class LocationComponent extends FormControlSuperclass<Location>
location!: Location;
// This looks unnecessary, but is required for Angular to provide `Injector`
constructor(injector: Injector)
super(injector);
handleIncomingValue(value: Location)
this.location = value;
modifyLocation<K extends keyof Location>(field: K, value: Location[K])
this.location = ...this.location, [field]: value ;
this.emitOutgoingValue(this.location);
然后,您可以将 <app-location>
与 [(ngModel)]
、[formControl]
、自定义验证器一起使用 - 您可以使用 Angular 开箱即用的控件进行的所有操作。
【讨论】:
【参考方案6】:当您可以使用内部 ngModel 时,为什么要创建新的值访问器。每当您创建一个包含 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>
【讨论】:
虽然这看起来很有希望,但由于您调用的是 super,因此缺少“扩展” 是的,我没有在这里复制我的整个代码并且忘记删除 super()。 另外,outerNgModel 是从哪里来的?完整的代码会更好地提供这个答案 根据angular.io/docs/ts/latest/api/core/index/…innerNgModel
定义在ngAfterViewInit
这根本不起作用。 innerNgModel 永远不会被初始化,outerNgModel 永远不会被声明,传递给构造函数的 ngModel 永远不会被使用。【参考方案7】:
使用ControlValueAccessor
NG_VALUE_ACCESSOR
很容易做到这一点。
您可以阅读这篇文章来制作一个简单的自定义字段 Create Custom Input Field Component with Angular
【讨论】:
以上是关于Angular 2 自定义表单输入的主要内容,如果未能解决你的问题,请参考以下文章
自定义 angular2 表单输入组件,在组件内具有两种方式绑定和验证