Angular4 - 没有表单控件的值访问器
Angular4 - No value accessor for form control
我有一个自定义元素:
<div formControlName="surveyType">
<div *ngFor="let type of surveyTypes"
(click)="onSelectType(type)"
[class.selected]="type === selectedType">
<md-icon>{{ type.icon }}</md-icon>
<span>{{ type.description }}</span>
</div>
</div>
当我尝试添加 formControlName 时,收到一条错误消息:
ERROR Error: No value accessor for form control with name:
'surveyType'
我尝试添加 ngDefaultControl
但没有成功。
好像是因为没有input/select...不知道怎么办
我想将我的点击绑定到此 formControl,以便当有人点击整张卡片时将我的 'type' 推入 formControl。可能吗?
您应该在 input
而不是 div
上使用 formControlName="surveyType"
您只能在实现 ControlValueAccessor
.
的指令上使用 formControlName
实现接口
所以,为了做你想做的事,你必须创建一个实现 ControlValueAccessor
的组件,这意味着 实现以下三个功能:
writeValue
(告诉 Angular 如何将模型中的值写入视图)
registerOnChange
(注册一个视图改变时调用的处理函数)
registerOnTouched
(注册一个处理程序,当组件接收到触摸事件时调用,有助于了解组件是否已获得焦点)。
注册提供商
然后,你必须告诉 Angular 这个指令是一个 ControlValueAccessor
(接口不会削减它,因为当 TypeScript 被编译成 JavaScript 时它会从代码中剥离).您可以通过 注册提供商 来完成此操作。
供应商应提供NG_VALUE_ACCESSOR
and use an existing value. You'll also need a forwardRef
here. Note that NG_VALUE_ACCESSOR
should be a multi provider.
例如,如果您的自定义指令名为 MyControlComponent,您应该在传递给 @Component
装饰器的对象中添加以下内容:
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyControlComponent),
}
]
用法
您的组件可以使用了。使用 template-driven forms,ngModel
绑定现在可以正常工作。
有了 reactive forms,您现在可以正确使用 formControlName
,表单控件将按预期运行。
资源
对我来说,这是由于 select 输入控件上的 "multiple" 属性,因为 Angular 对这种类型的控件有不同的 ValueAccessor。
const countryControl = new FormControl();
并且在模板内部像这样使用
<select multiple name="countries" [formControl]="countryControl">
<option *ngFor="let country of countries" [ngValue]="country">
{{ country.name }}
</option>
</select>
更多详情参考 Official Docs
错误意味着,当您将 formControl
放在 div
上时,Angular 不知道该怎么做。
要解决此问题,您有两种选择。
- 您将
formControlName
放在一个开箱即用的 Angular 支持的元素上。它们是:input
、textarea
和 select
。
- 您实现了
ControlValueAccessor
接口。通过这样做,你告诉 Angular "how to access the value of your control"(因此得名)。或者简单来说:当您将 formControlName
放在一个元素上时,该元素自然没有与之关联的值,该怎么办。
现在,实现 ControlValueAccessor
接口一开始可能有点令人生畏。特别是因为那里没有太多好的文档,您需要在代码中添加大量样板文件。因此,让我尝试通过一些简单易行的步骤来分解它。
将表单控件移动到它自己的组件中
为了实现ControlValueAccessor
,您需要创建一个新组件(或指令)。将与表单控件相关的代码移到那里。像这样它也可以很容易地重复使用。控件已经在组件内部可能是首要原因,为什么您需要实现 ControlValueAccessor
接口,否则您将无法将自定义组件与 Angular 表单一起使用。
将样板添加到您的代码中
实现 ControlValueAccessor
接口非常冗长,这是它附带的样板文件:
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// a) copy paste this providers property (adjust the component name in the forward ref)
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// c) copy paste this code
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// d) copy paste this code
writeValue(input: string) {
// TODO
}
那么各个部分在做什么?
- a) 让 Angular 在运行时知道您实现了
ControlValueAccessor
接口
- b) 确保你正在实现
ControlValueAccessor
接口
- c) 这可能是最令人困惑的部分。基本上你正在做的是,你给 Angular 在运行时用它自己的实现覆盖你的 class properties/methods
onChange
和 onTouch
的方法,这样然后你可以调用这些函数。所以理解这一点很重要:你不需要自己实现 onChange 和 onTouch(最初的空实现除外)。您对 (c) 所做的唯一一件事就是让 Angular 将它自己的功能附加到您的 class。为什么?所以你可以在适当的时候调用Angular提供的onChange
和onTouch
方法。我们将在下面看到它是如何工作的。
- d) 我们还将在下一节中实现
writeValue
方法的工作原理。我把它放在这里,所以 ControlValueAccessor
上的所有必需属性都已实现,您的代码仍然可以编译。
实现写入值
writeValue
的作用是 在自定义组件内部做一些事情,当表单控件在外部发生变化时 。因此,例如,如果您已将自定义表单控件组件命名为 app-custom-input
并且您将在父组件中使用它,如下所示:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
然后 writeValue
会在父组件以某种方式更改 myFormControl
的值时触发。例如,这可能是在表单初始化期间 (this.form = this.formBuilder.group({myFormControl: ""});
) 或表单重置 this.form.reset();
.
如果表单控件的值在外部发生变化,您通常想要做的是将其写入表示表单控件值的局部变量。例如,如果您的 CustomInputComponent
围绕基于文本的表单控件展开,它可能如下所示:
writeValue(input: string) {
this.input = input;
}
并且在 CustomInputComponent
的 html 中:
<input type="text"
[ngModel]="input">
您也可以按照 Angular 文档中的描述将其直接写入输入元素。
现在您已经处理了当外部发生变化时组件内部发生的情况。现在让我们看看另一个方向。当您的组件内部发生变化时,您如何通知外界?
调用onChange
下一步是通知父组件有关 CustomInputComponent
内部的更改。这就是上面 (c) 中的 onChange
和 onTouch
函数发挥作用的地方。通过调用这些函数,您可以通知外部组件内部的更改。为了将值的更改传播到外部,您需要 以新值作为参数调用 onChange。例如,如果用户在自定义组件的 input
字段中键入内容,您将使用更新后的值调用 onChange
:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
如果您再次检查上面的实现 (c),您会看到发生了什么:Angular 将它自己的实现绑定到 onChange
class 属性 .该实现需要一个参数,即更新后的控件值。您现在正在做的是调用该方法,从而让 Angular 知道更改。 Angular 现在将继续并更改外部的表单值。这是这一切的关键部分。 您通过调用 onChange
告诉 Angular 何时应该更新表单控件以及使用什么值。你已经给了它 "access the control value".
的方法
顺便说一句:onChange
这个名字是我选的。您可以在此处选择任何内容,例如 propagateChange
或类似内容。不管你怎么命名它,它都是接受一个参数的同一个函数,它由 Angular 提供,并且在运行时通过 registerOnChange
方法绑定到你的 class。
调用onTouch
由于表单控件可以是 "touched",您还应该提供 Angular 了解何时触摸您的自定义表单控件的方法。您可以通过调用 onTouch
函数来做到这一点,您猜对了。因此,对于我们这里的示例,如果您想遵守 Angular 对开箱即用的表单控件的处理方式,您应该在输入字段模糊时调用 onTouch
:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
同样,onTouch
是我选择的名称,但它的实际功能是由 Angular 提供的,它采用零参数。这是有道理的,因为您只是让 Angular 知道表单控件已被触及。
综合起来
那么,当它们组合在一起时,它看起来如何?它应该是这样的:
// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// Step 1: copy paste this providers property
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// Step 3: Copy paste this stuff here
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// Step 4: Define what should happen in this component, if something changes outside
input: string;
writeValue(input: string) {
this.input = input;
}
// Step 5: Handle what should happen on the outside, if something changes on the inside
// in this simple case, we've handled all of that in the .html
// a) we've bound to the local variable with ngModel
// b) we emit to the ouside by calling onChange on ngModelChange
}
// custom-input.component.html
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>
// OR
<form [formGroup]="form" >
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
更多示例
- 输入示例:https://stackblitz.com/edit/angular-control-value-accessor-simple-example-tsmean
- 延迟加载输入示例:https://stackblitz.com/edit/angular-control-value-accessor-lazy-input-example-tsmean
- 带有按钮的示例:https://stackblitz.com/edit/angular-control-value-accessor-button-example-tsmean
嵌套表单
请注意,控件值访问器不是嵌套表单组的正确工具。对于嵌套表单组,您可以简单地使用 @Input() subform
代替。控制值访问器是为了包装 controls
,而不是 groups
!请参阅此示例如何使用嵌套表单的输入:https://stackblitz.com/edit/angular-nested-forms-input-2
来源
我有一个自定义元素:
<div formControlName="surveyType">
<div *ngFor="let type of surveyTypes"
(click)="onSelectType(type)"
[class.selected]="type === selectedType">
<md-icon>{{ type.icon }}</md-icon>
<span>{{ type.description }}</span>
</div>
</div>
当我尝试添加 formControlName 时,收到一条错误消息:
ERROR Error: No value accessor for form control with name: 'surveyType'
我尝试添加 ngDefaultControl
但没有成功。
好像是因为没有input/select...不知道怎么办
我想将我的点击绑定到此 formControl,以便当有人点击整张卡片时将我的 'type' 推入 formControl。可能吗?
您应该在 input
而不是 div
formControlName="surveyType"
您只能在实现 ControlValueAccessor
.
formControlName
实现接口
所以,为了做你想做的事,你必须创建一个实现 ControlValueAccessor
的组件,这意味着 实现以下三个功能:
writeValue
(告诉 Angular 如何将模型中的值写入视图)registerOnChange
(注册一个视图改变时调用的处理函数)registerOnTouched
(注册一个处理程序,当组件接收到触摸事件时调用,有助于了解组件是否已获得焦点)。
注册提供商
然后,你必须告诉 Angular 这个指令是一个 ControlValueAccessor
(接口不会削减它,因为当 TypeScript 被编译成 JavaScript 时它会从代码中剥离).您可以通过 注册提供商 来完成此操作。
供应商应提供NG_VALUE_ACCESSOR
and use an existing value. You'll also need a forwardRef
here. Note that NG_VALUE_ACCESSOR
should be a multi provider.
例如,如果您的自定义指令名为 MyControlComponent,您应该在传递给 @Component
装饰器的对象中添加以下内容:
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyControlComponent),
}
]
用法
您的组件可以使用了。使用 template-driven forms,ngModel
绑定现在可以正常工作。
有了 reactive forms,您现在可以正确使用 formControlName
,表单控件将按预期运行。
资源
对我来说,这是由于 select 输入控件上的 "multiple" 属性,因为 Angular 对这种类型的控件有不同的 ValueAccessor。
const countryControl = new FormControl();
并且在模板内部像这样使用
<select multiple name="countries" [formControl]="countryControl">
<option *ngFor="let country of countries" [ngValue]="country">
{{ country.name }}
</option>
</select>
更多详情参考 Official Docs
错误意味着,当您将 formControl
放在 div
上时,Angular 不知道该怎么做。
要解决此问题,您有两种选择。
- 您将
formControlName
放在一个开箱即用的 Angular 支持的元素上。它们是:input
、textarea
和select
。 - 您实现了
ControlValueAccessor
接口。通过这样做,你告诉 Angular "how to access the value of your control"(因此得名)。或者简单来说:当您将formControlName
放在一个元素上时,该元素自然没有与之关联的值,该怎么办。
现在,实现 ControlValueAccessor
接口一开始可能有点令人生畏。特别是因为那里没有太多好的文档,您需要在代码中添加大量样板文件。因此,让我尝试通过一些简单易行的步骤来分解它。
将表单控件移动到它自己的组件中
为了实现ControlValueAccessor
,您需要创建一个新组件(或指令)。将与表单控件相关的代码移到那里。像这样它也可以很容易地重复使用。控件已经在组件内部可能是首要原因,为什么您需要实现 ControlValueAccessor
接口,否则您将无法将自定义组件与 Angular 表单一起使用。
将样板添加到您的代码中
实现 ControlValueAccessor
接口非常冗长,这是它附带的样板文件:
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// a) copy paste this providers property (adjust the component name in the forward ref)
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// c) copy paste this code
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// d) copy paste this code
writeValue(input: string) {
// TODO
}
那么各个部分在做什么?
- a) 让 Angular 在运行时知道您实现了
ControlValueAccessor
接口 - b) 确保你正在实现
ControlValueAccessor
接口 - c) 这可能是最令人困惑的部分。基本上你正在做的是,你给 Angular 在运行时用它自己的实现覆盖你的 class properties/methods
onChange
和onTouch
的方法,这样然后你可以调用这些函数。所以理解这一点很重要:你不需要自己实现 onChange 和 onTouch(最初的空实现除外)。您对 (c) 所做的唯一一件事就是让 Angular 将它自己的功能附加到您的 class。为什么?所以你可以在适当的时候调用Angular提供的onChange
和onTouch
方法。我们将在下面看到它是如何工作的。 - d) 我们还将在下一节中实现
writeValue
方法的工作原理。我把它放在这里,所以ControlValueAccessor
上的所有必需属性都已实现,您的代码仍然可以编译。
实现写入值
writeValue
的作用是 在自定义组件内部做一些事情,当表单控件在外部发生变化时 。因此,例如,如果您已将自定义表单控件组件命名为 app-custom-input
并且您将在父组件中使用它,如下所示:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
然后 writeValue
会在父组件以某种方式更改 myFormControl
的值时触发。例如,这可能是在表单初始化期间 (this.form = this.formBuilder.group({myFormControl: ""});
) 或表单重置 this.form.reset();
.
如果表单控件的值在外部发生变化,您通常想要做的是将其写入表示表单控件值的局部变量。例如,如果您的 CustomInputComponent
围绕基于文本的表单控件展开,它可能如下所示:
writeValue(input: string) {
this.input = input;
}
并且在 CustomInputComponent
的 html 中:
<input type="text"
[ngModel]="input">
您也可以按照 Angular 文档中的描述将其直接写入输入元素。
现在您已经处理了当外部发生变化时组件内部发生的情况。现在让我们看看另一个方向。当您的组件内部发生变化时,您如何通知外界?
调用onChange
下一步是通知父组件有关 CustomInputComponent
内部的更改。这就是上面 (c) 中的 onChange
和 onTouch
函数发挥作用的地方。通过调用这些函数,您可以通知外部组件内部的更改。为了将值的更改传播到外部,您需要 以新值作为参数调用 onChange。例如,如果用户在自定义组件的 input
字段中键入内容,您将使用更新后的值调用 onChange
:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
如果您再次检查上面的实现 (c),您会看到发生了什么:Angular 将它自己的实现绑定到 onChange
class 属性 .该实现需要一个参数,即更新后的控件值。您现在正在做的是调用该方法,从而让 Angular 知道更改。 Angular 现在将继续并更改外部的表单值。这是这一切的关键部分。 您通过调用 onChange
告诉 Angular 何时应该更新表单控件以及使用什么值。你已经给了它 "access the control value".
顺便说一句:onChange
这个名字是我选的。您可以在此处选择任何内容,例如 propagateChange
或类似内容。不管你怎么命名它,它都是接受一个参数的同一个函数,它由 Angular 提供,并且在运行时通过 registerOnChange
方法绑定到你的 class。
调用onTouch
由于表单控件可以是 "touched",您还应该提供 Angular 了解何时触摸您的自定义表单控件的方法。您可以通过调用 onTouch
函数来做到这一点,您猜对了。因此,对于我们这里的示例,如果您想遵守 Angular 对开箱即用的表单控件的处理方式,您应该在输入字段模糊时调用 onTouch
:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
同样,onTouch
是我选择的名称,但它的实际功能是由 Angular 提供的,它采用零参数。这是有道理的,因为您只是让 Angular 知道表单控件已被触及。
综合起来
那么,当它们组合在一起时,它看起来如何?它应该是这样的:
// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// Step 1: copy paste this providers property
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// Step 3: Copy paste this stuff here
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// Step 4: Define what should happen in this component, if something changes outside
input: string;
writeValue(input: string) {
this.input = input;
}
// Step 5: Handle what should happen on the outside, if something changes on the inside
// in this simple case, we've handled all of that in the .html
// a) we've bound to the local variable with ngModel
// b) we emit to the ouside by calling onChange on ngModelChange
}
// custom-input.component.html
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>
// OR
<form [formGroup]="form" >
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
更多示例
- 输入示例:https://stackblitz.com/edit/angular-control-value-accessor-simple-example-tsmean
- 延迟加载输入示例:https://stackblitz.com/edit/angular-control-value-accessor-lazy-input-example-tsmean
- 带有按钮的示例:https://stackblitz.com/edit/angular-control-value-accessor-button-example-tsmean
嵌套表单
请注意,控件值访问器不是嵌套表单组的正确工具。对于嵌套表单组,您可以简单地使用 @Input() subform
代替。控制值访问器是为了包装 controls
,而不是 groups
!请参阅此示例如何使用嵌套表单的输入:https://stackblitz.com/edit/angular-nested-forms-input-2