内容简介:本文是学习 Angular ControlValueAccessor 过程中的笔记摘要,全文结构如下:本文假设你已经了解了基本的 Angular 语法与表单用法,其中所使用的完整代码可以在 GIST 上找到
本文是学习 Angular ControlValueAccessor 过程中的笔记摘要,全文结构如下:
- 表单与控件介绍 - 通过日常在 Angular 中使用表单以及自定义控件的场景引出 ControlValueAccessor
- ControlValueAccessor API 介绍 - 介绍 ControlValueAccessor API 的细节
- 自定义表单控件实现 - 通过一个计数器例子实战来叙述如何利用 ControlValueAccessor 自定义你的表单控件
- 初始化计数器控件
- 完善 ControlValueAccessor 接口
- 注册 NG_VALUE_ACCESSOR 提供者
- 在响应式表单中使用
本文假设你已经了解了基本的 Angular 语法与表单用法,其中所使用的完整代码可以在 GIST 上找到 https://gist.github.com/hijiangtao/1be6feba90294863a403316b3b9dc31e ,以下开始正文。
1. 表单与控件介绍
不仅仅是在 Angular 中,大部分 Web 应用中(尤其是中后台系统)用表单来处理用户输入都是非常基础的功能。在 Angular 中,一个极其简单的表单应用如下所示:
import { Component } from '@angular/core'; import { FormControl } from '@angular/forms'; @Component({ selector: 'app-reactive-favorite-color', template: ` Favorite Color: <input type="text" [formControl]="favoriteColorControl"> ` }) export class FavoriteColorComponent { favoriteColorControl = new FormControl(''); }
以上例子中,我们在控件类中定义了一个 FormControl
属性,然后在模版中通过 [formControl]
将 UI (DOM) 与数据进行双向绑定。
但写代码时为了以后场景复用,难免要自定义控件,当控件中涉及到用户输入交互时,可能就要与表单打交道。在 Angular 中,不管你使用模板驱动表单还是响应式表单,总是会创建 FormControl
。如果你使用响应式表单,你需要显式创建 FormControl
对象,并使用 formControl
或 formControlName
指令来绑定原生控件;如果你使用模板驱动表单, FormControl
对象会被 NgModel
指令隐式创建。
这时,一个直观但不优雅的做法是在调用自定义控件时通过 @Input
传入一个表单数据 ,而控件内部写相应的逻辑对 Angular 表单进行操作。具体调用可能长成这样:
<form [formGroup]="anyForm"> <some-component [form]="anyForm"></some-component> <another-component [form]="anyForm"></another-component> </form>
但是为了让控件拥有 Angular 表单般的使用体验,我希望 API 设计成这样,直接通过 formControlName
来绑定数据:
<form [formGroup]="anyForm"> <some-component formControlName="cool" /></some-component> <another-component type="text" formControlName="coool"></another-component> </form>
2. ControlValueAccessor API 介绍
在 Angular 中,类似的自定义表单其实是有相应接口实现的,这里就用到我们今天要谈的主角——ControlValueAccessor,其官方定义是
Implement this interface to create a custom form control directive that integrates with Angular forms.
简单来说,这个对象桥接原生表单控件和 formControl
指令,并同步两者的值。
任何一个控件或指令都可以通过实现 ControlValueAccessor
接口并注册为 NG_VALUE_ACCESSOR
,从而转变成 ControlValueAccessor
类型的对象。在介绍其他内容之前,我们来看看 ControlValueAccessor
的数据结构:
interface ControlValueAccessor { writeValue(obj: any): void registerOnChange(fn: any): void registerOnTouched(fn: any): void setDisabledState(isDisabled: boolean)?: void }
这个接口定义有四个方法,为了描述准确,我们将 Angular 表单和原生表单分别用 Angular form 和 Native form 区分开:
- writeValue:任何
FormControl
显式调用 API 的 值 操作都将调用自定义表单控件的writeValue()
方法,并将新值作为参数传入。其作用是设置原生表单控件的值。 数据流向则是从 Angular form :arrow_right: Native form。 - registerOnChange:注册
onChange
事件,在初始化时被调用,参数为事件触发函数。这个事件触发函数有什么用呢?我们首先要在registerOnChange
中将该事件触发函数保存起来,等到合适的时候(比如控件收到用户输入,需要作出响应时)调用该函数以触发事件。这里的概念比较绕,你可以将这个函数理解成“当控件内表单数据存在变化、需要通知控件的调用方所用到的一个 API”,即将控件内信息同步出去。这个事件函数的参数是 Angular form 所要接收的值。 数据流向从 Native form :arrow_right: Angular form。 - registerOnTouched:注册
onTouched
事件,即用户和控件交互时触发的回调函数。该函数用于通知表单控件已经处于 touched 状态,以更新绑定的FormContorl
的内部状态。 数据流向从 Native form :arrow_right: Angular form。 - setDisabledState:当表单状态变为
DISABLED
或从DISABLED
变更时,表单 API 会调用setDisabledState()
方法,以启用或禁用对应的 DOM 元素。 数据流向则是从 Angular form :arrow_right: Native form。
关于其中的交互关系我们可以参照下面这张图:
3. 自定义表单控件实现
介绍完概念后,我们通过一个例子来看看如何自定义表单控件。我们分以下几个步骤
3.1 初始化自定义控件
一个基本的 counter 控件应该包含 ts 脚本和 html 文件:
-
counter.component.html
<div> <p>当前值: 8</p> <button (click)="increment()"> + </button> <button (click)="decrement()"> - </button> </div>
-
counter.component.ts
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-counter', templateUrl: './counter.component.html', styleUrls: ['./counter.component.css'], }) export class Counter { @Input() count: number = 0; ngOnInit() {} increment() { this.count++; } decrement() { this.count--; } }
3.2 完善 ControlValueAccessor 接口
根据 ControlValueAccessor 定义的 API 接口,当合法值输入控件时,我们需要更新控件内的 count 值,所以我们这样完善 writeValue 方法:
writeValue(value: any) { if (value) { this.count = value; } }
然后我们注册两个事件,涉及到 registerOnChange 方法和 registerOnTouched 方法:
propagateOnChange: (value: any) => void = (_: any) => {}; propagateOnTouched: (value: any) => void = (_: any) => {}; registerOnChange(fn: any) { this.propagateOnChange = fn; } registerOnTouched(fn: any) { this.propagateOnTouched = fn; }
由于控件内 count 改变时我们需要调用事件触发函数将数据传递给 Angular form,于是针对控件内部的 count 我们做一些改造:
_count: number = 0; get count() { return this._count; } set count(value: number) { this._count = value; this.propagateOnChange(this._count); }
好了,其余内容不变。
3.3 注册 NG_VALUE_ACCESSOR 提供者
对于我们开发的自定义控件来说,实现 ControlValueAccessor 接口只完成了一半工作,我们还需要执行注册操作。 NG_VALUE_ACCESSOR
提供者用来指定实现了 ControlValueAccessor
接口的类,并且被 Angular 用来和 formControl
同步,通常是使用控件类或指令来注册。所有表单指令都是使用 NG_VALUE_ACCESSOR
标识来注入控件值访问器,然后选择合适的访问器。具体分以下几个步骤:
步骤1:创建 EXE_COUNTER_VALUE_ACCESSOR
export const EXE_COUNTER_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Counter), multi: true };
步骤2:配置控件 providers 信息
@Component({ ... providers: [EXE_COUNTER_VALUE_ACCESSOR], ... }) export class Counter implements ControlValueAccessor { ... }
这部分我们不做深入探讨,关于 NG_VALUE_ACCESSOR 及相关概念的深入,我将部分内容附在文末的参考列表中了。
3.4 在响应式表单中使用
至此,我们的控件就写完了。由于 Angular 存在响应式表单和模版驱动表单两种,以下以响应式表单为例,我们实际调用它看看效果。首先是导入控件并注册 [ReactiveFormsModule](https://angular.cn/api/forms/ReactiveFormsModule)
:
import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; // 替换为你自己的 counter 控件地址 import { Counter } from './components/counter/counter.component'; @NgModule({ declarations: [ ... Counter, ], imports: [ ... ReactiveFormsModule, ], }) export class AppModule { }
然后我们完善调用自定义控件的代码,假设我们是在一个叫 hello 的 component 中调用它,那么代码分以下两步,首先是 HTML:
<ng-container> <p>counter 控件测试</p> <form [formGroup]="helloForm"> <app-counter formControlName="counter"></app-counter> </form> </ng-container>
其次,我们完善 ts 代码:
import { Component, OnInit, Input } from '@angular/core'; import { FormGroup, FormBuilder } from '@angular/forms'; @Component({ selector: 'app-hello-page', templateUrl: './hello.component.html', styleUrls: ['./hello.component.css'], }) export class HelloPageComponent implements OnInit { helloForm: FormGroup; constructor(private fb: FormBuilder) { } ngOnInit() { this.helloForm = this.fb.group({ counter: 5 // 设置初始值 }); } }
以上便是 Angular ControlValueAccessor 在表单控件中从定义到使用的全部过程,以下附上计数器的完整 TypeScript 代码,其余完整代码见 GIST https://gist.github.com/hijiangtao/1be6feba90294863a403316b3b9dc31e
参考
- Angualr API https://angular.io/api/forms/ControlValueAccessor
- Never again be confused when implementing ControlValueAccessor in Angular forms https://indepth.dev/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms/
- [译] 别再对 Angular 表单的 ControlValueAccessor 感到迷惑 https://github.com/AngularID-CN/AngularID-CN/blob/master/articles/angular-32.[翻译]-别再对-Angular-Form-的-ControlValueAccessor-感到迷惑.md
- What is forwardRef in Angular and why we need it https://blog.angularindepth.com/what-is-forwardref-in-angular-and-why-we-need-it-6ecefb417d48
- What does forwardRef do in angular? https://stackoverflow.com/questions/50894571/what-does-do-forwardref-in-angular
- https://segmentfault.com/a/1190000009070500
- https://blog.csdn.net/oschina_41790905/article/details/101716227
- https://angular.io/guide/reactive-forms
附 counter.component.ts 代码
import { Component, Input, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; export const EXE_COUNTER_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Counter), multi: true }; @Component({ selector: 'app-counter', templateUrl: './counter.component.html', styleUrls: ['./counter.component.css'], providers: [EXE_COUNTER_VALUE_ACCESSOR] }) export class Counter implements ControlValueAccessor { _count: number = 0; get count() { return this._count; } set count(value: number) { this._count = value; this.propagateOnChange(this._count); } propagateOnChange: (value: any) => void = (_: any) => {}; propagateOnTouched: (value: any) => void = (_: any) => {}; ngOnInit() {} writeValue(value: any) { if (value) { this.count = value; } } registerOnChange(fn: any) { this.propagateOnChange = fn; } registerOnTouched(fn: any) { this.propagateOnTouched = fn; } increment() { this.count++; } decrement() { this.count--; } }
以上所述就是小编给大家介绍的《Angular ControlValueAccessor - 自定义表单控件介绍与实战》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- vue 表单控件与绑定(七)
- Angular 使用 ControlValueAccessor 创建自定义表单控件
- Angular 从入坑到挖坑 - 表单控件概览
- Angular ControlValueAccessor:自定义表单控件介绍与实战
- Chrome 83 Beta 发布:与 Edge 合作改进的表单控件
- Chrome 83 稳定版发布:新的跨域政策、表单控件,和改进的 Web 体验
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。