formControl
该错误意味着,当您将 a 放在 a上时,Angular 不知道该怎么做div
。要解决此问题,您有两个选择。
- 您将
formControlName
Angular 放在一个开箱即用的元素上。它们是:input
和。textarea
select
- 您实现
ControlValueAccessor
接口。通过这样做,您是在告诉 Angular“如何访问控件的值”(因此得名)。或者简单来说:做什么,当你把 aformControlName
放在一个元素上时,它自然没有与之关联的值。
现在,实现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 一种方法来覆盖你的类属性/方法
onChange
,并onTouch
在运行时使用它自己的实现,这样你就可以调用这些函数。所以理解这一点很重要:你不需要自己实现onChange和onTouch(除了最初的空实现)。您对 (c) 所做的唯一一件事就是让 Angular 将它自己的函数附加到您的类中。为什么?因此,您可以在适当的时候调用Angular 提供的onChange
和onTouch
方法。我们将在下面看到它是如何工作的。
writeValue
d)当我们实现它时,我们还将在下一节中看到该方法是如何工作的。我已经把它放在这里,所以所有必需的属性ControlValueAccessor
都已实现,并且您的代码仍然可以编译。
实现 writeValue
什么writeValue
是在您的自定义组件内部做一些事情,当表单控件在外部更改时。例如,如果您已命名自定义表单控件组件app-custom-input
,并且您将在父组件中使用它,如下所示:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
然后writeValue
每当父组件以某种方式更改myFormControl
. 例如,这可能是在 form( this.form = this.formBuilder.group({myFormControl: ""});
) 的初始化期间或在表单重置时this.form.reset();
。
如果表单控件的值在外部发生变化,您通常想要做的是将其写入表示表单控件值的局部变量。例如,如果您CustomInputComponent
围绕基于文本的表单控件展开,它可能如下所示:
writeValue(input: string) {
this.input = input;
}
并在 html 中CustomInputComponent
:
<input type="text"
[ngModel]="input">
您也可以将其直接写入 Angular 文档中描述的输入元素。
现在您已经处理了当外部发生变化时组件内部发生的事情。现在让我们看看另一个方向。当组件内部发生变化时,如何通知外界?
调用 onChange
下一步是通知父组件你的CustomInputComponent
. 这就是上面 (c) 中的onChange
andonTouch
函数发挥作用的地方。通过调用这些函数,您可以告知外部组件内部的更改。为了将值的更改传播到外部,您需要使用新值作为参数调用 onChange。例如,如果用户input
在自定义组件的字段中键入内容,onChange
则使用更新后的值调用:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
如果您再次从上面检查实现 (c),您会看到发生了什么:Angular 将它自己的实现绑定到onChange
类属性。该实现需要一个参数,即更新后的控制值。您现在正在做的是调用该方法,从而让 Angular 知道更改。Angular 现在将继续并更改外部的表单值。这是所有这一切的关键部分。你告诉 Angular 什么时候应该更新表单控件,并通过调用onChange
. 您已经为它提供了“访问控制值”的方法。
顺便说一句:名字onChange
是我选的。你可以在这里选择任何东西,例如propagateChange
或类似的。不管你怎么命名它,它将是一个接受一个参数的函数,它由 Angular 提供,并且registerOnChange
在运行时通过方法绑定到你的类。
调用 onTouch
由于可以“触摸”表单控件,因此您还应该让 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>
更多示例
嵌套表格
请注意,控制值访问器不是嵌套表单组的正确工具。对于嵌套表单组,您可以简单地使用 an@Input() subform
代替。控制值访问器旨在包装controls
,而不是groups
!请参阅此示例如何将输入用于嵌套表单:https ://stackblitz.com/edit/angular-nested-forms-input-2
来源