【荐】深入Angular自定义表单控件

推荐文章

Maxim Koretskyi
Never again be confused when implementing ControlValueAccessor in Angular forms

推荐理由

在大型复杂的管理后台项目中,很有可能你会遇到需要自定义表单控件(Custom form control)。很多文章都介绍了此时应该定义ControlValueAccessor,也展示了如何实现,但并没有说出为什么,这个类在Angular的表单架构中起了什么作用。该文章就解决了为什么的问题,让你从原理理解自定义表单控件。

文章概要

首先,只要你创建表单,Angular就会创建对应FormControl,无论是模板驱动表单还是响应式表单。模板驱动表单的FormControl是由NgModel指令隐性创建,而响应式表单是由你自己创建,通过FormControlName指令将Angular表单元素和原生表单元素进行绑定。

1
2
3
4
5
6
7
// packages/forms/src/directives/ng_model.ts
@Directive({
selector: '[ngModel]...',
...
})
export class NgModel ... {
_control = new FormControl(); <---------------- 这里

也就是说在Angular中的表单,不是原生表单,而是封装后的Angular表单。不仅仅是原生的表单控件可以处理封装成Angular表单,其他自定义的Angular组件也可以,只要定义了ControlValueAccessor

ControlValueAccessor是什么呢?它是原生元素和Angular表单之间的桥梁,将组件或者指令继承ControlValueAccessor的接口就能变成Angular表单使用了。

ControlValueAccessor接口长这样:

1
2
3
4
5
6
7
// packages/forms/src/directives/control_value_accessor.ts
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
...
}

writeValue是在初始化的时候将formControl的值传递给原生表单控件;registerOnChange用来获取原生表单控件的值更新时通知Angular表单控件更新的函数;registerOnTouched用来获取通知用户正在交互的函数。

image

明确来说,那些原生表单控件都有其对应的ControlValueAccessor

1
2
3
4
5
6
7
8
9
10
+------------------------------------+----------------------+
| Accessor | Form Element |
+------------------------------------+----------------------+
| DefaultValueAccessor | input, textarea |
| CheckboxControlValueAccessor | input[type=checkbox] |
| NumberValueAccessor | input[type=number] |
| RadioControlValueAccessor | input[type=radio] |
| RangeValueAccessor | input[type=range] |
| SelectControlValueAccessor | select |
| SelectMultipleControlValueAccessor | select[multiple] |

那原生表单控件和Angular表单控件保持一致的原理是什么呢?

我们看下formControl指令的实现:

1
2
3
4
5
6
7
// packages/forms/src/directives/reactive_directives/form_control_directive.ts
export class FormControlDirective ... {
...
ngOnChanges(changes: SimpleChanges): void {
if (this._isControlChanged(changes)) {
setUpControl(this.form, this);

formControl指令调用了setUpControl函数来实现formControlControlValueAccessor之间的交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// packages/forms/src/directives/shared.ts
export function setUpControl(control: FormControl, dir: NgControl) {
// 初始化原生表单控件
dir.valueAccessor.writeValue(control.value);
// 监听原生表单控件,将值同步给form control
dir.valueAccessor.registerOnChange((newValue: any) => {
...
control.setValue(newValue, {emitModelToViewChange: false});
});
// 反之,监听form control,将值同步给原生表单控件
control.registerOnChange((newValue: any, ...) => {
dir.valueAccessor.writeValue(newValue);
});

到此,我们应该明白ControlValueAccessor中定义writeValue等函数是怎么work了吧。

以上就是最重要的原理部分。

接下来,作者通过第三方组件jquery-slider来演示了如何用Angular封装第三方组件库,以及如何将该组件封装成自定义表单控件。具体教程可以看原文。

延伸阅读

本文首发知乎野草。如有不当之处,欢迎指正。