Angular4-表单控件没有值访问器

我有一个自定义元素:

<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 时,我得到一个错误消息:

错误: 没有名称为: 调查类型

我试图添加 ngDefaultControl,但没有成功。 似乎是因为没有输入/选择... 我不知道该怎么办。

我希望将我的点击绑定到这个 formControl,以便当有人点击整个卡片时,将我的“类型”推入 formControl。有可能吗?

293628 次浏览

你应该在 input上使用 formControlName="surveyType",而不是在 div

只能在实现 ControlValueAccessor的指令上使用 formControlName

实现接口

因此,为了完成您想要的任务,您必须创建一个实现 ControlValueAccessor的组件,这意味着 实现以下三个功能:

  • writeValue(告诉角度如何将值从模型写入视图)
  • registerOnChange(注册一个在视图更改时调用的处理程序函数)
  • registerOnTouched(注册一个处理程序,当组件接收到一个触摸事件时调用该处理程序,这对于了解组件是否已被集中非常有用)。

注册一个提供者

然后,你必须告诉 Angular 这个指令是一个 ControlValueAccessor(接口不会删除它,因为当 TypeScript 被编译成 JavaScript 时,它从代码中被剥离出来)。你用 注册供应商做这个。

提供商应该提供 NG_VALUE_ACCESSOR使用现有价值。在这里您还需要一个 forwardRef。请注意,NG_VALUE_ACCESSOR应该是一个 多供应商

例如,如果您的自定义指令名为 MyControlComponent,那么您应该在传递给 @Component装饰器的对象中添加以下代码行:

providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyControlComponent),
}
]

用法

您的组件已经可以使用了。使用 模板驱动的表单ngModel绑定现在可以正常工作了。

使用 反应形式,您现在可以正确地使用 formControlName,并且窗体控件的行为将与预期的一样。

资源

对我来说,这是由于选择输入控件的“多”属性,因为 Angular 对这种类型的控件有不同的 ValueAccessor。

const countryControl = new FormControl();

内部模板使用如下

    <select multiple name="countries" [formControl]="countryControl">
<option *ngFor="let country of countries" [ngValue]="country">
\{\{ country.name }}
</option>
</select>

详情请参阅 官方文件

这个错误意味着,当你把 formControl放在 div上的时候,Angular 不知道该怎么做。 要解决这个问题,你有两个选择。

  1. 你把 formControlName放在一个元素上,这个元素是由现成的 Angular 支持的。这些是: inputtextareaselect
  2. 实现 ControlValueAccessor接口。通过这样做,您告诉 Angular“如何访问您的控件的值”(因此得名)。或者简单地说: 当你把一个 formControlName放在一个元素上,这个元素自然不会有一个与之相关联的值。

现在,实现 ControlValueAccessor接口一开始可能有点令人畏惧。特别是因为这方面的文档不多,您需要向代码中添加大量的样板文件。因此,让我尝试用一些简单的步骤来分解这个问题。

将窗体控件移动到它自己的组件中

为了实现 ControlValueAccessor,您需要创建一个新组件(或指令)。将与窗体控件相关的代码移到此处。像这样,它也将很容易重复使用。在组件中已经有一个控件可能是首要的原因,为什么您需要实现 ControlValueAccessor接口,因为否则您将无法将您的自定义组件与角度形式一起使用。

将样板文件添加到代码中

实现 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 一些方法,用它自己的实现覆盖类属性/方法 onChangeonTouch,这样您就可以调用这些函数。因此,理解这一点很重要: 你不需要在 Change 和 onTouch 上实现它们(除了最初的空实现)。使用(c)的唯一方法是让 Angular 将它自己的函数附加到类中。为什么?所以你可以在适当的时候用 打电话提供的 onChangeonTouch方法。我们将在下面看到它是如何工作的。
  • D)当我们实现 writeValue方法时,我们还将在下一节中看到它是如何工作的。我已经把它放在这里了,所以 ControlValueAccessor上所有必需的属性都已经实现,代码仍然可以编译。

实现 writeValue

writeValue所做的就是对 当窗体控件在外部发生更改时,在自定义组件内执行某些操作。例如,如果您已经将自定义表单控件组件命名为 app-custom-input,并且您将在父组件中使用它,如下所示:

<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

然后,只要父组件以某种方式改变 myFormControl的值,就会触发 writeValue。这可能是例如在初始化表单(this.form = this.formBuilder.group({myFormControl: ""});)期间或在表单重置 this.form.reset();时。

如果窗体控件的值在外部发生更改,通常需要将其写入表示窗体控件值的局部变量。例如,如果您的 CustomInputComponent围绕着一个基于文本的表单控件,它可以是这样的:

writeValue(input: string) {
this.input = input;
}

以及 CustomInputComponent的 html:

<input type="text"
[ngModel]="input">

您也可以将它直接写入到 Angular 文档中描述的 input 元素中。

现在您已经处理了当组件外部发生变化时在组件内部发生的情况。现在让我们看看另一个方向。当组件内部发生变化时,如何通知外部世界?

呼唤改变

下一步是将 CustomInputComponent内部的更改通知父组件。这就是来自(c)的 onChangeonTouch发挥作用的地方。通过调用这些函数,您可以将组件内部的更改通知给外部。为了将值的更改传播到外部,您需要 使用新值作为参数调用 onChange。例如,如果用户在自定义组件的 input字段中键入某些内容,则使用更新后的值调用 onChange:

<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">

如果您再次检查上面的实现(c) ,您将看到发生了什么: 角度绑定它自己的实现到 onChange类属性。该实现需要一个参数,即更新后的控件值。你现在要做的就是调用这个方法让 Angular 知道这个变化。角度现在将继续前进,并改变外部的形式值。这才是关键。您通过调用 onChange告诉 Angular 什么时候应该更新窗体控件以及使用什么值.你已经给了它“访问控制值”的方法。

顺便说一下: onChange这个名字是我选的。您可以在这里选择任何东西,例如 propagateChange或类似的。不管你怎么命名它,它都是同一个函数,只接受一个参数,这个参数由 Angular 提供,在运行时由 registerOnChange方法绑定到你的类。

调用 onTouch

由于可以“触摸”窗体控件,所以还应该让 Angular 了解自定义窗体控件何时被触摸。您猜对了,您可以通过调用 onTouch函数来完成。因此,对于我们这里的例子,如果你想保持顺从的角度是如何做的开箱即用的形式控制,你应该调用 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>

更多例子

嵌套表格

注意,控制值访问器不是嵌套表单组的正确工具。对于嵌套的表单组,只需使用 @Input() subform即可。控制值访问器应该包装 controls,而不是 groups!请参阅此示例,了解如何将输入用于嵌套表单: https://stackblitz.com/edit/angular-nested-forms-input-2

消息来源