在angular里我们一般都是给input元素添加[(ngModel)]="value"实现数据双向绑定,如果想实现自定义的组件上实现ngModel双向数据绑定应该怎么办呐。。。
网上找了一下,没看懂记录一下。
场景:组件能获取父组件通过ngModel绑定的值,能通过ngModel改变父组件对应的数据。如下代码:
<app-child [(ngModel])="appData"></app-child>
1、先贴出效果图:
2、下面是app-child组件的代码:
import { Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-child', templateUrl: './child.component.html', styleUrls: ['./child.component.css'], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ChildComponent), multi: true }] }) export class ChildComponent implements ControlValueAccessor { constructor() { } _data: any; add () { this.childData ++; } change = (value: any) => {}; // 先定义一个方法,很重要,用于接收registerOnChange()方法里传递回来的方法,然后通过这个方法就能通知到外部组件数据更新。 set childData(value: number) { // childData被更改走该方法 this._data = value; this.change(this._data); // 将更新后的数据通知到外部组件 } get childData() { // 页面或者方法里面有调用childData就会走该方法 return this._data; } writeValue(val): void { // 初始化时,获取并监听父组件通过ngModel传递进来的数据 if (val) { this._data = val; } } registerOnChange(fn: any): void { // 初始化后,执行该方法,并保存控件接收到 change 事件后,调用的函数 this.change = fn; } registerOnTouched(fn: any): void { } }
3、下面开始说下实现的过程吧:
如果添加ngModel后报如下错误,检查组件对应的Module文件有没有导入FormsModule
import { FormsModule } from '@angular/forms';
@NgModule({
...
imports: [
...,
FormsModule
],
...
})
import FormsModule后,控制台任然会报错:
这是因为我们需要在使用ngModel的组件里实现ControlValueAccessor的接口方法。
先引入和使用我们必须使用的配置:
import { Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-child', templateUrl: './child.component.html', styleUrls: ['./child.component.css'], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ChildComponent), // 这里的组件名为当前组件的名字 multi: true }] }) export class ChildComponent implements ControlValueAccessor { constructor() { } childData = 2; }
处理完成后控制台的报错信息已经改变:
这是因为ControlValueAccessor的接口有几个必须存在的方法,会自动去调用:
writeValue(val): void { } registerOnChange(fn: any): void { } registerOnTouched(fn: any): void { }
- 初始化的时候调用
writeValue()
方法,将会使用表单模型中对应的初始值作为参数(也就是ngModel里的值)。 - registerOnChange() 可以用来通知外部,组件已经发生变化。
- registerOnTouched() 方法用于设置当控件接收到 touched 事件后,调用的函数。
知道了这三个方法后,我们就可以在writeValue方法里给组件设置父组件通过ngModel传递过来的值了。如:
writeValue(val): void { if (val) { this.childData = val; } }
那么怎么将组件里更新的数据传递给父组件呐。
registerOnChange(fn: any): void { // 初始化后,执行该方法,并保存控件接收到 change 事件后,调用的函数 this.change = fn; }
writeValue()方法后就会执行registerOnChange()方法,我们就是通过该方法传递回来的方法参数来通知到外部组件数据更新的,所以我们要在最开始就定义一个方法来接收。
change = (value: any) => {}; // 先定义一个方法,很重要,用于接收registerOnChange()方法里传递回来的方法,然后通过这个方法就能通知到外部组件数据更新。
然后就可以通过change方法通知外部组件了
set childData(value: number) { // childData被更改走该方法 this._data = value; this.change(this._data); // 将更新后的数据通知到外部组件 }
最开始贴出来的代码,中间使用了set 和get去处理了数据,在get childData()方法里打断点发现会执行很多次该方法,其实也可以修改成通过更新数据的时候就直接调用change()方法来通知外部组件数据更新,如下:
import { Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-child', templateUrl: './child.component.html', styleUrls: ['./child.component.css'], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ChildComponent), multi: true }] }) export class ChildComponent implements ControlValueAccessor { constructor() { } _data: any; childData = 1; add () { this.childData ++; this.change(this.childData); } change = (value: any) => {}; // 先定义一个方法,很重要,用于接收registerOnChange()方法里传递回来的方法,然后通过这个方法就能通知到外部组件数据更新。 writeValue(val): void { // 初始化时,获取并监听父组件通过ngModel传递进来的数据 if (val) { this.childData = val; } } registerOnChange(fn: any): void { // 初始化后,执行该方法,并保存控件接收到 change 事件后,调用的函数 this.change = fn; } registerOnTouched(fn: any): void { } }
中间不用使用get和set,不知道两种方法哪种更好。
其实通过子组件通知父级组件数据更新,可以使用@Input和@Output来实现的,如果是@Input获取的父级组件的数据,父级组件数据更新,子组件需要在ngOnChanges生命周期里去监听对应的数据变更并处理相应的逻辑。
不过在自定义组件上使用ngModel实现数据的双向绑定还可以用作表单处理上,比如表单模板和表单验证。