适用于一切双向数据绑定,不局限与下面的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个网址
双向绑定底层原理简单案例
双向绑定的过程