之前在想要复杂的表单控件的时候,直接使用第三方组件库里的组件就行了,但在抛弃第三上方组件库 的时候,问题就到了自己手上,如何创建一个带有复杂功能的表单控件,趁这个机遇,也是尝试了编写自定义的表单控件。
为什么要自定义表单控件
原生的表单控件有input, select, textarea等等,并且angualr都封装好了,可以直接绑定模型,与angular进行交互。但是大多数时候原生控件并不能满足我们的需求,这时候就得自定义表单控件,但在自定义控件的同时,我们也想使用类似formControl这样的指令绑定模型,让angular帮我们完成其他复杂的事情,使我们自定义的控件就好像原生的控件一样。这里我就以我写的助选组件为例子。
实现ControlValueAccessor接口
不得不说,angular实在太强大了,我们只要实现ControlValueAccessor接口,就可以将我们自定义的表单控件像原生控件一样使用了,来看看angular是怎么做到这一点的。
首先先看看ControlValueAccessor接口的方法:
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
仅仅就四个方法,我们说表单控件主要做的事,无非为两点。一:从dom/视图的数据到模型。二:从模型数据到dom/视图。这四个接口,就完成了我们需要的功能。
首先writeValue方法,我们注意表单控件是可以设置值的,这个方法就是angualr向我们的控件写入值。在我们助选组件中,这个值就是选中的值,即suggestion:
suggestion: any;
writeValue(obj: any): void {
if (obj) {
this.suggestion = obj;
}
}
助选组件的v层代码:
<input type="text" placeholder="item..." class="form-control" (input)="searchEvent(searchBox.value)"
[ngModel]="showText"
[placeholder]="placeholder" #searchBox/>
<div class="dd" id="nestable" *ngIf="showSuggestions">
<ol class="dd-list show-list">
<li class="dd-item" *ngFor="let suggestion of _suggestions" (click)="suggestionChange(suggestion)">
<div class="dd-handle">
<ng-template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{$implicit: suggestion}"></ng-template>
</div>
</li>
</ol>
</div>
显而易见,是由一个输入框和一个下拉列表产生的,表单控件设置值的时候,输入框应该显示输入的值,因此我们在writeValue中进行一些额外的操作:为input设置值
// 显示文字
showText: string;
suggestion: FormControl;
// 显示的字段
@Input() field: string;
writeValue(obj: any): void {
if (obj) {
this.suggestion = obj;
// 设置默认选中
this.setText(this.suggestion);
}
}
// 设置文本框文字
setText(suggestion: any) {
this.showText = suggestion;
if (this.field) {
this.field.split('.').forEach((filed: string) => {
if (this.showText) {
this.showText = this.showText[filed];
}
});
}
}
这里使用了一个field的input字段,这是因为你并不直到suggestion的类型是什么,如果是字符串直接显示是没问题的,但如果是对象,则需要显示的字段,比如object.showText.text这样的
registerOnChange(fn: any): void方法:顾名思义,这个方法就是注册change事件,当我们组件的值发生改变时,我们需要更新模型的数据,模型通过registerOnChange接口注册OnChange事件,在我们组件内部则可以任意选择触发的时机。在助选组件中,当点击下拉列表时,我们则更新模型数据
propagateChange = (_: any) => {
}
// 注册change函数
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
// 选中下拉框时
suggestionChange(suggestion: any) {
if (suggestion) {
this.suggestion = suggestion;
this.propagateChange(suggestion);
this.setText(suggestion);
}
}
registerOnTouched(fn: any): void则与registerOnChange相同,同理setDisabledState(可以不实现)就是设置我们组件可不可见。简单来说,我们实现了ControlValueAccessor接口后,则可完成模型到视图/dom之间的双向交互了。但还需进行最后的一步,我们的自定义组件才可以使用。
提供自定义组件
我们知道,js里是没有接口这个概念的,因此在ts编译为js后,ControlValueAccessor接口的信息就失去了,angualr的解决方法是通过令牌获取信息,这是依赖注入的概念,首先看一下如何提供:
@Component({
selector: 'app-auto-complete',
templateUrl: './auto-complete.component.html',
styleUrls: ['./auto-complete.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutoCompleteComponent),
multi: true,
},
],
})
export class AutoCompleteComponent implements OnInit, ControlValueAccessor {
......
}
首先我们需要提供NG_VALUE_ACCESSOR这个令牌,他的类型是这样的:
export declare const NG_VALUE_ACCESSOR: InjectionToken<ControlValueAccessor>;
可以发现他就是ControlValueAccessor的注入令牌,用来保存接口信息,这里使用useExisting提供,因为我们的组件一般通过选择器(<app-auto-complete></app-auto-complete>)就已经创建存在了,所以要使用存在的AutoCompleteComponent对象,不然则会重新创建一个AutoCompleteComponent实例,与我们页面上的并不是同一个对象,就无法进行交互。forwardRef()则是为了打破循环依赖。
总结qi
到此位置,自定义组件就可以结合angualr进行使用了,而外的需求我们都可以在此基础上实现,当然还有验证这一方面,但时间匆忙,就到这里了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。