惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
Latest news
Latest news
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
美团技术团队
V
Visual Studio Blog
F
Full Disclosure
腾讯CDC
H
Help Net Security
D
DataBreaches.Net
M
MIT News - Artificial intelligence
罗磊的独立博客
博客园 - 司徒正美
N
Netflix TechBlog - Medium
U
Unit 42
Vercel News
Vercel News
I
InfoQ
S
SegmentFault 最新的问题
B
Blog RSS Feed
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
博客园_首页
The GitHub Blog
The GitHub Blog
T
Tailwind CSS Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
NISL@THU
NISL@THU
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
Y
Y Combinator Blog
S
Schneier on Security
P
Proofpoint News Feed
T
Tenable Blog
Cloudbric
Cloudbric
博客园 - 【当耐特】
The Register - Security
The Register - Security
人人都是产品经理
人人都是产品经理
Google DeepMind News
Google DeepMind News
C
Cyber Attacks, Cyber Crime and Cyber Security
量子位
A
Arctic Wolf
N
News and Events Feed by Topic
H
Hacker News: Front Page
MongoDB | Blog
MongoDB | Blog
The Hacker News
The Hacker News
Webroot Blog
Webroot Blog

mafeifan 的编程技术分享

mafengwo-mp3-downloader | mafeifan 的编程技术分享 示例页面 | mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 查看 default namespace 下的 default service account名称 mafeifan 的编程技术分享 检查日志 | mafeifan 的编程技术分享 bridge fdb show dev flannel.1 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 数据收集原理分析 | mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 几种方法 | mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 check the export on the very top of this document so we can use $do mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 mafeifan 的编程技术分享 运维自动化之ANSIBLE | mafeifan 的编程技术分享 延伸 -- 关于前端路由 xdebug 3 和 2 的区别
mafeifan 的编程技术分享
2026-01-16 · via mafeifan 的编程技术分享

原文链接:Never again be confused when implementing ControlValueAccessor in Angularforms

如果你正在做一个复杂项目,必然会需要自定义表单控件,这个控件主要需要实现ControlValueAccessor接口(译者注:该接口定义方法可参考**API 文档说明,也可参考Angular 源码定义**)。网上有大量文章描述如何实现这个接口,但很少说到它在 Angular 表单架构里扮演什么角色,如果你不仅仅想知道如何实现,还想知道为什么这样实现,那本文正合你的胃口。

首先我解释下为啥需要ControlValueAccessor接口以及它在 Angular 中是如何使用的。然后我将展示如何封装第三方组件作为 Angular 组件,以及如何使用输入输出机制实现组件间通信(译者注:Angular 组件间通信输入输出机制可参考**官网文档),最后将展示如何使用ControlValueAccessor来实现一种针对 Angular 表单**新的数据通信机制。

FormControl 和 ControlValueAccessor ​

如果你之前使用过 Angular 表单,你可能会熟悉**FormControl,Angular 官方文档将它描述为追踪单个表单控件值和有效性的实体对象。需要明白,不管你使用模板驱动还是响应式表单(译者注:即模型驱动),FormControl都总会被创建。如果你使用响应式表单,你需要显式创建FormControl对象,并使用formControlformControlName指令来绑定原生控件;如果你使用模板驱动方法,FormControl对象会被NgModel指令隐式创建(译者注:可查看 Angular 源码这一行**):

@Directive({
  selector: '[ngModel]...',
  ...
})
export class NgModel ... {
  _control = new FormControl();   <---------------- here

不管formControl是隐式还是显式创建,都必须和原生 DOM 表单控件如input,textarea进行交互,并且很有可能需要自定义一个表单控件作为 Angular 组件而不是使用原生表单控件,而通常自定义表单控件会封装一个使用纯 JS 写的控件如**jQuery UI's Slider。本文我将使用原生表单控件**术语来区分 Angular 特定的formControl和你在html使用的表单控件,但你需要知道任何一个自定义表单控件都可以和formControl指令进行交互,而不是原生表单控件如input

原生表单控件数量是有限的,但是自定义表单控件是无限的,所以 Angular 需要一种通用机制来桥接原生/自定义表单控件和formControl指令,而这正是**ControlValueAccessor**干的事情。这个对象桥接原生表单控件和formControl指令,并同步两者的值。官方文档是这么描述的(译者注:为清晰理解,该描述不翻译):

ControlValueAccessoracts as a bridge between the Angular forms API and a native element in the DOM.

任何一个组件或指令都可以通过实现ControlValueAccessor接口并注册为NG_VALUE_ACCESSOR,从而转变成ControlValueAccessor类型的对象,稍后我们将一起看看如何做。另外,这个接口还定义两个重要方法——writeValueregisterOnChange(译者注:可查看 Angular 源码**这一行**):

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  ...
}

formControl指令使用writeValue方法设置原生表单控件的值(译者注:你可能会参考**L186L41);使用registerOnChange方法来注册由每次原生表单控件值更新时触发的回调函数(译者注:你可能会参考这三行,L186L43,以及L85),你需要把更新的值传给这个回调函数,这样对应的 Angular 表单控件值也会更新(译者注:这一点可以参考 Angular 它自己写的DefaultValueAccessor的写法是如何把 input 控件每次更新值传给回调函数的,L52L89);使用registerOnTouched方法来注册用户和控件交互时触发的回调(译者注:你可能会参考L95**)。

下图是Angular 表单控件如何通过ControlValueAccessor来和原生表单控件交互的(译者注:formControl和**你写的或者 Angular 提供的CustomControlValueAccessor**两个都是要绑定到 native DOM element 的指令,而formControl指令需要借助CustomControlValueAccessor指令/组件,来和 native DOM element 交换数据。):

image.png

再次强调,不管是使用响应式表单显式创建还是使用模板驱动表单隐式创建,ControlValueAccessor都总是和 Angular 表单控件进行交互。

Angular 也为所有原生 DOM 表单元素创建了Angular表单控件(译者注:Angular 内置的 ControlValueAccessor):

AccessorForm Element
DefaultValueAccessorinput,textarea
CheckboxControlValueAccessorinput[type=checkbox]
NumberValueAccessorinput[type=number]
RadioControlValueAccessorinput[type=radio]
RangeValueAccessorinput[type=range]
SelectControlValueAccessorselect
SelectMultipleControlValueAccessorselect[multiple]

从上表中可看到,当 Angular 在组件模板中中遇到inputtextareaDOM 原生控件时,会使用DefaultValueAccessor指令:

@Component({
  selector: 'my-app',
  template: `
      <input [formControl]="ctrl">
  `
})
export class AppComponent {
  ctrl = new FormControl(3);
}

所有表单指令,包括上面代码中的formControl指令,都会调用**setUpControl函数来让表单控件和DefaultValueAccessor实现交互(译者注:意思就是上面代码中绑定的formControl指令,在其自身实例化时,会调用setUpControl()函数给同样绑定到inputDefaultValueAccessor指令做好安装工作,如L85**,这样formControl指令就可以借助DefaultValueAccessor来和input元素交换数据了)。细节可参考formControl指令的代码:

export class FormControlDirective ... {
  ...
  ngOnChanges(changes: SimpleChanges): void {
    if (this._isControlChanged(changes)) {
      setUpControl(this.form, this);

还有setUpControl函数源码也指出了原生表单控件和 Angular 表单控件是如何数据同步的(译者注:作者贴的可能是 Angular v4.x 的代码,v5 有了点小小变动,但基本相似):

export function setUpControl(control: FormControl, dir: NgControl) {

  // initialize a form control
  // 调用 writeValue() 初始化表单控件值
  dir.valueAccessor.writeValue(control.value);

  // setup a listener for changes on the native control
  // and set this value to form control
  // 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新
  valueAccessor.registerOnChange((newValue: any) => {
    control.setValue(newValue, {emitModelToViewChange: false});
  });

  // setup a listener for changes on the Angular formControl
  // and set this value to the native control
  // 设置 Angular 表单控件值更新监听器,每当 Angular 表单控件值更新,原生控件值也更新
  control.registerOnChange((newValue: any, ...) => {
    dir.valueAccessor.writeValue(newValue);
  });

只要我们理解了内部机制,就可以实现我们自定义的 Angular 表单控件了。

组件封装器 ​

由于 Angular 为所有默认原生控件提供了控件值访问器,所以在封装第三方插件或组件时,需要写一个新的控件值访问器。我们将使用上文提到的 jQuery UI 库的**slider**插件,来实现一个自定义表单控件吧。

简单的封装器 ​

最基础实现是通过简单封装使其能在屏幕上显示出来,所以我们需要一个NgxJquerySliderComponent组件,并在其模板里渲染出slider

@Component({
  selector: 'ngx-jquery-slider',
  template: `
      <div #location></div>
  `,
  styles: ['div {width: 100px}']
})
export class NgxJquerySliderComponent {
  @ViewChild('location') location;
  widget;
  ngOnInit() {
    this.widget = $(this.location.nativeElement).slider();
  }
}

这里我们使用标准的jQuery方法在原生 DOM 元素上创建一个slider控件,然后使用widget属性引用这个控件。

一旦简单封装好了slider组件,我们就可以在父组件模板里使用它:

@Component({
  selector: 'my-app',
  template: `
      <h1>Hello {{name}}</h1>
      <ngx-jquery-slider></ngx-jquery-slider>
  `
})
export class AppComponent { ... }

为了运行程序我们需要加入jQuery相关依赖,简化起见,在index.html中添加全局依赖:

<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">

这里是安装依赖的**源码**。

交互式表单控件 ​

上面的实现还不能让我们自定义的slider控件与父组件交互,所以还得使用输入/输出绑定来是实现组件间数据通信:

export class NgxJquerySliderComponent {
  @ViewChild('location') location;
  @Input() value;
  @Output() private valueChange = new EventEmitter();
  widget;

  ngOnInit() {
    this.widget = $(this.location.nativeElement).slider();   
    this.widget.slider('value', this.value);
    this.widget.on('slidestop', (event, ui) => {
      this.valueChange.emit(ui.value);
    });
  }

  ngOnChanges() {
    if (this.widget && this.widget.slider('value') !== this.value) {
      this.widget.slider('value', this.value);
    }
  }
}

一旦slider组件创建,就可以订阅slidestop事件获取变化的值,一旦slidestop事件被触发了,就可以使用输出事件发射器valueChanges通知父组件。当然我们也可以使用ngOnChanges生命周期钩子来追踪输入属性value值的变化,一旦其值变化,我们就将该值设置为slider控件的值。

然后就是父组件中如何使用slider组件的代码实现:

<ngx-jquery-slider
    [value]="sliderValue"
    (valueChange)="onSliderValueChange($event)">
</ngx-jquery-slider>

**源码**在这里。

但是,我们想要的是,使用slider组件作为表单的一部分,并使用模板驱动表单或响应式表单的指令与其数据通信,那就需要让其实现ControlValueAccessor接口了。由于我们将实现的是新的组件通信方式,所以不需要标准的输入输出属性绑定方式,那就移除相关代码吧。(译者注:作者先实现标准的输入输出属性绑定的通信方式,又要删除,主要是为了引入新的表单组件交互方式,即ControlValueAccessor。)

实现自定义控件值访问器 ​

实现自定义控件值访问器并不难,只需要两步:

  1. 注册NG_VALUE_ACCESSOR提供者
  2. 实现ControlValueAccessor接口

NG_VALUE_ACCESSOR提供者用来指定实现了ControlValueAccessor接口的类,并且被 Angular 用来和formControl同步,通常是使用组件类或指令来注册。所有表单指令都是使用NG_VALUE_ACCESSOR标识来注入控件值访问器,然后选择合适的访问器(译者注:这句话可参考这两行代码,L175L181)。要么选择DefaultValueAccessor或者内置的数据访问器,否则 Angular 将会选择自定义的数据访问器,并且有且只有一个自定义的数据访问器(译者注:这句话参考**selectValueAccessor源码实现**)。

让我们首先定义提供者:

@Component({
  selector: 'ngx-jquery-slider',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: NgxJquerySliderComponent,
    multi: true
  }]
  ...
})
class NgxJquerySliderComponent implements ControlValueAccessor {...}

我们直接在组件装饰器里直接指定类名,然而 Angular 源码默认实现是放在类装饰器外面:

export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DefaultValueAccessor),
  multi: true
};
@Directive({
  selector:'input',
  providers: [DEFAULT_VALUE_ACCESSOR]
  ...
})
export class DefaultValueAccessor implements ControlValueAccessor {}

放在外面就需要使用forwardRef,关于原因可以参考**What is forwardRef in Angular and why we need it**。当实现自定义controlValueAccessor,我建议还是放在类装饰器里吧(译者注:个人建议还是学习 Angular 源码那样放在外面)。

一旦定义了提供者后,就让我们实现controlValueAccessor接口:

export class NgxJquerySliderComponent implements ControlValueAccessor {
  @ViewChild('location') location;
  widget;
  onChange;
  value;

ngOnInit() {
    this.widget = $(this.location.nativeElement).slider(this.value);
   this.widget.on('slidestop', (event, ui) => {
      this.onChange(ui.value);
    });
}

writeValue(value) {
    this.value = value;
    if (this.widget && value) {
      this.widget.slider('value', value);
    }
  }

registerOnChange(fn) { this.onChange = fn;  }

registerOnTouched(fn) {  }

由于我们对用户是否与组件交互不感兴趣,所以先把registerOnTouched置空吧。在registerOnChange里我们简单保存了对回调函数fn的引用,回调函数是由formControl指令传入的(译者注:参考**L85**),只要每次slider组件值发生改变,就会触发这个回调函数。在writeValue方法内我们把得到的值传给slider组件。

现在我们把上面描述的功能做成一张交互式图:

image.png

如果你把简单封装和controlValueAccessor封装进行比较,你会发现父子组件交互方式是不一样的,尽管封装的组件与slider组件的交互是一样的。你可能注意到formControl指令实际上简化了与父组件交互的方式。这里我们使用writeValue来向子组件写入数据,而在简单封装方法中使用ngOnChanges;调用this.onChange方法输出数据,而在简单封装方法中使用this.valueChange.emit(ui.value)

现在,实现了ControlValueAccessor接口的自定义slider表单控件完整代码如下:

@Component({
  selector: 'my-app',
  template: `
      <h1>Hello {{name}}</h1>
      <span>Current slider value: {{ctrl.value}}</span>
      <ngx-jquery-slider [formControl]="ctrl"></ngx-jquery-slider>
      <input [value]="ctrl.value" (change)="updateSlider($event)">
  `
})
export class AppComponent {
  ctrl = new FormControl(11);

  updateSlider($event) {
    this.ctrl.setValue($event.currentTarget.value, {emitModelToViewChange: true});
  }
}

你可以查看程序的**最终实现**。

Github ​

项目的**Github 仓库**。