之前在想要复杂的表单控件的时候,直接使用第三方组件库里的组件就行了,但在抛弃第三上方组件库 的时候,问题就到了自己手上,如何创建一个带有复杂功能的表单控件,趁这个机遇,也是尝试了编写自定义的表单控件。

参考文章

为什么要自定义表单控件

原生的表单控件有input, select, textarea等等,并且angualr都封装好了,可以直接绑定模型,与angular进行交互。但是大多数时候原生控件并不能满足我们的需求,这时候就得自定义表单控件,但在自定义控件的同时,我们也想使用类似formControl这样的指令绑定模型,让angular帮我们完成其他复杂的事情,使我们自定义的控件就好像原生的控件一样。这里我就以我写的助选组件为例子。

clipboard.png

实现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进行使用了,而外的需求我们都可以在此基础上实现,当然还有验证这一方面,但时间匆忙,就到这里了。

参考链接: angualr customer form control


鲸冬香
456 声望27 粉丝

引用和评论

0 条评论