typescript Angular 4-表单控件无值访问器

5lhxktic  于 2023-10-22  发布在  TypeScript
关注(0)|答案(5)|浏览(119)

我有一个自定义元素:

<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错误:名称为:的表单控件没有值访问器:'surveyType'
我尝试添加ngDefaultControl没有成功。似乎是因为没有输入/选择…我不知道该怎么办
我想把我的点击绑定到这个formControl,以便当有人点击整个卡片时,将我的“类型”推到formControl中。有可能吗?

moiiocjp

moiiocjp1#

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

实现接口

所以,为了做你想做的事情,你必须创建一个实现ControlValueAccessor的组件,这意味着实现以下三个函数

  • writeValue(告诉Angular如何将值从模型写入视图)
  • registerOnChange(注册一个在视图改变时调用的处理函数)
  • registerOnTouched(注册一个当组件接收到触摸事件时要调用的处理程序,用于了解组件是否已被聚焦)。

注册provider

然后,你必须告诉Angular这个指令是一个ControlValueAccessor(接口不会削减它,因为当TypeScript编译为JavaScript时,它会从代码中剥离出来)。您可以通过注册一个提供商来完成此操作。
提供者应该提供NG_VALUE_ACCESSOR并使用现有值。你还需要一个forwardRef。请注意,NG_VALUE_ACCESSOR应该是multi provider
例如,如果您的自定义指令名为MyControlComponent,则应在传递给@Component装饰器的对象中添加以下内容:

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

用法

您的组件已准备好使用。对于模板驱动的表单,ngModel绑定现在可以正常工作了。
有了reactive forms,您现在可以正确使用formControlName,表单控件将按预期运行。

资源

nxagd54h

nxagd54h2#

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

r6hnlfcb

r6hnlfcb3#

这个错误意味着,当你把一个formControl放在一个div上时,Angular不知道该怎么做。要解决这个问题,你有两个选择。
1.你把formControlName放在一个元素上,这是Angular开箱即用的支持。它们是:inputtextareaselect
1.您实现了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">

您也可以直接将其写入到input元素,如Angular文档中所述。
现在,您已经处理了当外部发生变化时组件内部发生的情况。现在让我们看看另一个方向。当组件内部发生变化时,如何通知外部世界?

调用onChange

下一步是通知父组件CustomInputComponent内部的更改。这就是上面(c)中的onChangeonTouch函数发挥作用的地方。通过调用这些函数,您可以将组件内部的更改通知外部。为了将值的更改传播到外部,您需要以新值作为参数调用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。控制值访问器应该 Package controls,而不是groups!请参阅此示例如何为嵌套表单使用输入:https://stackblitz.com/edit/angular-nested-forms-input-2

来源

50pmv0ei

50pmv0ei4#

1-打开app.module.ts
2-将ReactiveFormModule、FormsModule添加到@NgModule装饰器下的imports部分。

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属性应该可以正常工作。
我希望它能帮助

xfb7svmp

xfb7svmp5#

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

更多详情ref Official Docs

相关问题