适用于一切双向数据绑定,不局限与下面的input输入,文末链接1就是例子
Angular 中常见的 ControlValueAccessor 有:
-
DefaultValueAccessor - 用于
text
和textarea
类型的输入控件 -
SelectControlValueAccessor - 用于
select
选择控件 -
CheckboxControlValueAccessor - 用于
checkbox
复选控件
(注:妹的快写完的时候突然网页挂了,没保存到浪费了半小时重写)
一、问题的发现
公司最近的项目都是通过PrimeNG(ng2的UI组件)来开发,但别人的组件永远都够不着用,所以很有必要进行二次开发,或者自定义组件。今天看到项目中的大神把PrimeNG<p-autoComplete>的组件,自己自定义写了一份,趁有空我也研究了一下,发现里面存在着双向绑定的深层原理及使用方式。(我一直以为双向绑定原理就是[value]="value" (valueChange)="value=$event" 《揭秘Angular2》P202介绍)
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
这是ng2内置接口ControlValueAccessor
二、什么是ControlValueAccessor?
ControlValueAccessor 是一个接口,它的作用是:
-
把 form 模型中值映射到视图中
-
当视图发生变化时,通知 form directives 或 form controls
Angular 引入这个接口的原因是,不同的输入控件数据更新方式是不一样的。例如,对于我们常用的文本输入框来说,我们是设置它的 value
值,而对于复选框 (checkbox) 我们是设置它的 checked
属性。实际上,不同类型的输入控件都有一个 ControlValueAccessor
,用来更新视图。
这就是MVVM模型,Model -> View,View -> Model 之间的数据绑定
1、ControlValueAccessor接口底层代码
// angular2/packages/forms/src/directives/control_value_accessor.ts
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
- writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM 属性中。
- registerOnChange(fn: any):设置当控件接收到 change 事件后,调用的函数
- registerOnTouched(fn: any):设置当控件接收到 touched 事件后,调用的函数
- setDisabledState?(isDisabled: boolean):当控件状态变成
DISABLED
或从DISABLED
状态变化成ENABLE
状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素。
三、怎么使用达到双向绑定
import {Component, OnInit, Input, forwardRef} from '@angular/core';
import {API} from "app/share/lib/api/api";
import {NG_VALUE_ACCESSOR} from "@angular/forms";
// 封装一个对象,固定写法
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GoodSelectComponent),
multi: true
};
@Component({
selector: 'good-select',
templateUrl: './good-select.component.html',
styleUrls: ['./good-select.component.css'],
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class GoodSelectComponent implements OnInit {
/**
* 数据
*/
suggestions: any[];
@Input()
defaultLabel: string = "请选择…";
@Input()
multiSelect: boolean = false;
@Input()
width: string = "";
@Input()
height: string = "";
@Input()
valueField: string="";
@Input()
styleClass: string;
constructor(public api: API) {
}
ngOnInit() {
this.suggestions = [];
}
public onTouchedCallback: () => () => {};
public onChangeCallback: (_: any) => () => {};
public innerValue;
// 获取属性
get value(): any {
return this.innerValue;
};
// 设置属性,并触发监听器
set value(v: any) {
let vv = v && v.name?v.name:v;
this.innerValue = vv;
console.info(v)
if(this.valueField){
this.onChangeCallback(v[this.valueField]);
}else{
this.onChangeCallback(vv);
}
}
// 写入值
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
// 注册变化处理事件
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
// 注册触摸事件
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
/**
* 查询数据
* @param $event
*/
queryData($event: any) {
let value = $event.query;
let pageParms = {"first": 0, "rows": 9999};
this.api.call("abnormalOtherHandleController.waybillGoodsQuery", pageParms, {
name: value
}).ok(json => {
let result: any = json.result && json.result.content || [];
this.suggestions = result || [];
}).fail(err => {
throw new Error(err);
});
}
}
1、剖析
上面没看到接口ControlValueAccessor,其实NG_VALUE_ACCESSOR就是其别名。固定格式如下
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GoodSelectComponent),
multi: true
}
不知道说别名规范不规范,在别人的例子中都会继承ControlValueAccessor(implements ControlValueAccessor),但本文例子中就没有这种写法也可以实现双向数据绑定
这里面有两个比较重要的知识点
知识点1
1、当组件继承了ControlValueAccessor(implements ControlValueAccessor),那么writeValue、registerOnChange、registerOnTouched,在自定义组件过程中三者缺一不可,就相当于组件implements OnInit
其组件必须包含ngOnInit否则会报错
2、registerOnChange、registerOnTouched都是传入一个函数作为参数,所以上面都各自定义了一个空函数
public onTouchedCallback: () => () => {}; public onChangeCallback: (_: any) => () => {};
这里展开registerOnChange来讲,这个函数是用来监听视图层值(就是[(ngModle)]的值)的变化,当视图层值变化时会调用这个方法,实现视图层传值到模型层,writeValue相反。
知识点2
1、setter、getter的使用,老大说仅适合[(ngModle)]传进来的场景,但在我用了一个月来看,这种拦截适合父传子传参的一切输入属性,当视图层接收父组件传进来的[parentvalue]值([parentvalue]="value"),在子组件通过@Input() set parentvalue(value: any){ this._value = value},(注意:这里的parentvalue要和传进来的同名) get parentvalue() {return this._value} ,(注意:this._value会隐式替换[parentvalue]="value"这里的value值)
2.误区
这个案例也有误导的地方就是set/get和writeValue的混用,前面说了writeValue()
是M->V的过程,但这里set也是过滤拦截视图层的值的操作,那么究竟以哪个为准。
经过多次断点测试 writeValue()
这个方法好像只会在组件ngAfterViewInit之前调用,当过了这个生命周期就不会再进入该方法。初始化的时候,将会使用表单模型中对应的初始值作为参数,调用 writeValue()
方法,但往往不会都有初始值的情况,所以才会出现
writeValue(value: any) {
if (value !== this.innerValue) { // !== undefined null ''的情况出现
this.innerValue = value;
}
}
那么显然主导ngModel双向数据绑定的是set/get拦截器,阿里的NG-ZORRO的组件大量用到set/get拦截器也是这个原理
推荐2个网址
http://www.jianshu.com/p/a01015d5d83b 双向绑定底层原理简单案例
https://segmentfault.com/a/1190000009126012 双向绑定的过程