Angular ControlValueAccessor - 自定义表单控件介绍与实战

栏目: IT技术 · 发布时间: 4年前

内容简介:本文是学习 Angular ControlValueAccessor 过程中的笔记摘要,全文结构如下:本文假设你已经了解了基本的 Angular 语法与表单用法,其中所使用的完整代码可以在 GIST 上找到

本文是学习 Angular ControlValueAccessor 过程中的笔记摘要,全文结构如下:

  1. 表单与控件介绍 - 通过日常在 Angular 中使用表单以及自定义控件的场景引出 ControlValueAccessor
  2. ControlValueAccessor API 介绍 - 介绍 ControlValueAccessor API 的细节
  3. 自定义表单控件实现 - 通过一个计数器例子实战来叙述如何利用 ControlValueAccessor 自定义你的表单控件
    1. 初始化计数器控件
    2. 完善 ControlValueAccessor 接口
    3. 注册 NG_VALUE_ACCESSOR 提供者
    4. 在响应式表单中使用

本文假设你已经了解了基本的 Angular 语法与表单用法,其中所使用的完整代码可以在 GIST 上找到 https://gist.github.com/hijiangtao/1be6feba90294863a403316b3b9dc31e ,以下开始正文。

Angular ControlValueAccessor - 自定义表单控件介绍与实战

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 对象,并使用 formControlformControlName 指令来绑定原生控件;如果你使用模板驱动表单, 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。

关于其中的交互关系我们可以参照下面这张图:

Angular ControlValueAccessor - 自定义表单控件介绍与实战

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

参考

  1. Angualr API https://angular.io/api/forms/ControlValueAccessor
  2. Never again be confused when implementing ControlValueAccessor in Angular forms https://indepth.dev/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms/
  3. [译] 别再对 Angular 表单的 ControlValueAccessor 感到迷惑 https://github.com/AngularID-CN/AngularID-CN/blob/master/articles/angular-32.[翻译]-别再对-Angular-Form-的-ControlValueAccessor-感到迷惑.md
  4. 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
  5. What does forwardRef do in angular? https://stackoverflow.com/questions/50894571/what-does-do-forwardref-in-angular
  6. https://segmentfault.com/a/1190000009070500
  7. https://blog.csdn.net/oschina_41790905/article/details/101716227
  8. 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 - 自定义表单控件介绍与实战》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

SQL基础教程

SQL基础教程

MICK / 孙淼、罗勇 / 人民邮电出版社 / 2013-8-1 / CNY 69.00

本书介绍了关系数据库以及用来操作关系数据库的SQL语言的使用方法,提供了大量的示例程序和详实的操作步骤说明,读者可以亲自动手解决具体问题,循序渐进地掌握SQL的基础知识和技巧,切实提高自身的编程能力。在每章结尾备有习题,用来检验读者对该章内容的理解程度。另外本书还将重要知识点总结为“法则”,方便大家随时查阅。 本书适合完全没有或者具备较少编程和系统开发经验的初学者,也可以作为大中专院校的教材......一起来看看 《SQL基础教程》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换