Angular4 - 表单控件没有取值器

195

我有一个自定义元素:

<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,但失败了。 似乎是因为没有输入/选择... 我不知道该怎么做。

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


我不知道我的观点是:formControl 适用于 HTML 中的表单控件,但 div 不是表单控件。我想将我的 surveyType 绑定到我的 card div 的 type.id 上。 - jbtd
我知道我可以使用旧的Angular方式并将我的selectedType绑定到它,但我正在尝试使用并学习Angular 4中的响应式表单,并不知道如何在这种情况下使用formControl。 - jbtd
好的,可能这种情况不能由响应式表单处理。无论如何,谢谢 :) - jbtd
我在这里回答了如何将巨大的表单分解为子组件的问题 https://dev59.com/01kR5IYBdhLWcg3w0QNL#56375605,但这也适用于自定义控件值访问器。还可以查看 https://github.com/cloudnc/ngx-sub-form :) - maxime1992
我曾经遇到过同样的问题,并在这篇文章中解决了它: https://dev59.com/e1gQ5IYBdhLWcg3wlk_P#64617295 - Juri Sinitson
5个回答

324

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

实现接口

因此,为了实现您想要的效果,您需要创建一个组件,该组件实现ControlValueAccessor,这意味着要实现以下三个函数

  • writeValue(告诉Angular如何从模型中将值写入视图)
  • 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,并且表单控件将按预期运行。

资源


2
同时不要忘记在相关的输入框上添加ngDefaultControl。 - seawave_23
有没有可能直接在组件上包含ngDefaultControl指令,这样我们就不必每次都添加它了? 如果可以的话,请问如何实现? - Acuao

85

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


好的,但我不知道如何将我的卡片div转换为其他东西,以便成为HTML表单控件。 - jbtd
9
CustomValueAccessor的重点是将表单控件添加到“任何东西”,甚至是一个div。 - SoEzPz
6
@SoEzPz,虽然这是一种不好的模式。你在包装组件中模仿输入功能,自己重新实现标准的 HTML 方法(因此基本上是重新发明轮子,使您的代码冗长)。但在90%的情况下,您可以通过在包装器组件中使用<ng-content>来实现您想要的所有内容,并让定义formControls的父组件简单地将<input>放入<wrapper> 中。 - Phil
1
这个回答让我意识到模板中的 HTML 元素结构不完整。我在测试配置中添加了 MatSelectModule 导入,这为我解决了问题。 - fartwhif

37
该错误意味着,当您在
上放置formControl时,Angular不知道该怎么做。 要解决此问题,您有两个选项。
  1. formControlName放在受Angular支持的元素上。这些是:inputtextareaselect
  2. 实现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 在运行时覆盖你的类属性/方法 onChangeonTouch ,以便你可以调用这些函数。所以这一点很重要: 你不需要自己实现 onChange 和 onTouch(除了最初的空实现)。你使用 (c) 的唯一目的就是让 Angular 将它自己的函数附加到你的类上。为什么?因此你可以在适当的时候调用 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文档的描述将其直接写入输入元素。
现在,你已经处理了组件内部发生变化时外部的情况。现在让我们看看另一个方向。当你的组件内部发生变化时,如何通知外部世界?
调用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>

更多示例

嵌套表单

请注意,控件值访问器不是嵌套表单组的正确工具。对于嵌套表单组,您可以简单地使用一个 @Input() subform。控制值访问器用于包装 controls,而不是 groups!请参见此示例,了解如何为嵌套表单使用输入:https://stackblitz.com/edit/angular-nested-forms-input-2

来源


0
1-打开app.module.ts文件。
2-在@NgModule装饰器下的imports部分添加ReactiveFormModule和FormsModule。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // Adding FormsModule

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, ReactiveFormsModule,FormsModule], // Adding ReactiveFormModule, FormsModule
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

3-添加FormsModule后,刷新页面,formControlName属性应该能正常工作。
希望能帮到你。

-1

对我来说,这是由于选择输入控件上的“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>

更多细节请参考官方文档


3
“multiple” 是什么原因?我不明白你的代码如何解决问题,或者最初的问题是什么。你的代码展示了通常的基本用法。 - Lazar Ljubenović

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接