Angular4 - 表单控件没有值访问器
Posted
技术标签:
【中文标题】Angular4 - 表单控件没有值访问器【英文标题】:Angular4 - No value accessor for form control 【发布时间】:2018-01-21 10:04:52 【问题描述】:我有一个自定义元素:
<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。有可能吗?
【问题讨论】:
我不知道我的意思是:formControl 用于 html 中的表单控件,但 div 不是表单控件。我想你将我的surveyType与我的卡片div的type.id绑定 我知道我可以使用旧的角度方式并将我的 selectedType 绑定到它,但我试图使用和学习角度 4 的反应形式并且不知道如何在这种情况下使用 formControl。 好吧,也许只是这种情况不能通过反应形式来处理。无论如何谢谢:) 我已经在***.com/a/56375605/2398593 中回答了如何将巨大的表单分解为子组件,但这也适用于仅使用自定义控件值访问器的情况。另请查看github.com/cloudnc/ngx-sub-form :) 我遇到了同样的问题,并在这篇文章中解决了它:***.com/a/64617295/1190948 【参考方案1】:您只能在实现ControlValueAccessor
的指令上使用formControlName
。
实现接口
所以,为了做你想做的事,你必须创建一个实现ControlValueAccessor
的组件,这意味着实现以下三个功能:
writeValue
(告诉 Angular 如何将值从模型写入视图)
registerOnChange
(注册一个处理函数,当视图改变时调用)
registerOnTouched
(注册一个在组件接收到触摸事件时调用的处理程序,有助于了解组件是否已获得焦点)。
注册提供者
然后,你必须告诉 Angular 这个指令是一个ControlValueAccessor
(接口不会删掉它,因为当 TypeScript 被编译成 javascript 时它会从代码中剥离出来)。您可以通过注册提供者来做到这一点。
提供者应提供NG_VALUE_ACCESSOR
和use an existing value。您还需要在这里forwardRef
。注意NG_VALUE_ACCESSOR
应该是multi provider。
例如,如果您的自定义指令名为 MyControlComponent,则应在传递给 @Component
装饰器的对象中添加以下内容:
providers: [
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyControlComponent),
]
用法
您的组件已经可以使用了。使用template-driven forms,ngModel
绑定现在可以正常工作了。
使用reactive forms,您现在可以正确使用formControlName
,并且表单控件将按预期运行。
资源
Custom Form Controls in Angular by Thoughtram Angular Custom Form Controls with Reactive Forms and NgModel by Cory Rylan【讨论】:
也不要忘记相关输入上的 ngDefaultControl。【参考方案2】:您应该在input
上使用formControlName="surveyType"
而不是在div
上使用div
【讨论】:
是的,但是我不知道如何将我的卡片 div 变成其他的东西,这将是一个 html 表单控件 CustomValueAccessor 的重点是添加表单控件到任何东西,甚至是一个 div @SoEzPz 这是一个糟糕的模式。您在包装器组件中模仿 Input 功能,自己重新实现标准 HTML 方法(因此基本上是重新发明***并使您的代码冗长)。但在 90% 的情况下,您可以通过在包装器组件中使用<ng-content>
来完成您想要的一切,并让定义 formControls
的父组件只需将 放在 错误意味着,当您将formControl
放在div
上时,Angular 不知道该怎么做。
要解决此问题,您有两种选择。
-
您将
formControlName
放在一个元素上,Angular 开箱即用地支持该元素。它们是:input
、textarea
和 select
。
您实现了ControlValueAccessor
接口。通过这样做,您是在告诉 Angular“如何访问控件的值”(因此得名)。或者简单地说:当你在一个元素上放置一个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提供了在运行时用它自己的实现覆盖你的类属性/方法onChange
和onTouch
的方法,这样你就可以调用这些函数。所以理解这一点很重要:你不需要自己实现onChange和onTouch(除了最初的空实现)。您对 (c) 所做的唯一一件事就是让 Angular 将它自己的函数附加到您的类中。为什么?因此,您可以在适当的时候调用 Angular 提供的onChange
和onTouch
方法。我们将在下面看到它是如何工作的。
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 文档中的说明将其直接写入输入元素。
现在您已经处理了当外部发生变化时组件内部发生的事情。现在让我们看看另一个方向。当组件内部发生变化时,如何通知外界?
调用 onChange
下一步是将CustomInputComponent
内部的更改通知父组件。这就是上面 (c) 中的 onChange
和 onTouch
函数发挥作用的地方。通过调用这些函数,您可以告知外部组件内部的更改。为了将值的更改传播到外部,您需要以新值作为参数调用 onChange。例如,如果用户在您的自定义组件中的 input
字段中键入了一些内容,您可以使用更新后的值调用 onChange
:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
如果您再次检查上面的实现 (c),您会看到发生了什么:Angular 将它自己的实现绑定到 onChange
类属性。该实现需要一个参数,即更新后的控制值。您现在正在做的是调用该方法,从而让 Angular 知道更改。 Angular 现在将继续并更改外部的表单值。这是所有这一切的关键部分。 您通过调用onChange
告诉 Angular 何时应该更新表单控件以及更新的值是什么。您已经为它提供了“访问控制值”的方法。
顺便说一句: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>
更多示例
输入示例: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
来源
https://angular.io/api/forms/ControlValueAccessor https://www.tsmean.com/articles/angular/angular-control-value-accessor-example/【讨论】:
【参考方案4】:对我来说,这是由于选择输入控件上的“多个”属性,因为 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
【讨论】:
什么是由于“多个”?我看不到您的代码如何解决任何问题,或者原始问题是什么。您的代码显示了通常的基本用法。以上是关于Angular4 - 表单控件没有值访问器的主要内容,如果未能解决你的问题,请参考以下文章