angular - 我可以在 Angular 2+ 中访问自定义 ControlValueAccessor 的 formControl 吗?

标签 angular angular-reactive-forms

我想在 Angular 2+ 中创建一个带有 ControlValueAccessor 接口(interface)的自定义表单元素。此元素将是 <select> 的包装器.是否可以将 formControl 属性传播到包装元素?在我的例子中,验证状态没有传播到嵌套选择,如您在随附的屏幕截图中所见。

enter image description here

我的组件如下可用:

  const OPTIONS_VALUE_ACCESSOR: any = {
  multi: true,
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => OptionsComponent)
  };

  @Component({
  providers: [OPTIONS_VALUE_ACCESSOR], 
  selector: 'inf-select[name]',
  templateUrl: './options.component.html'
  })
  export class OptionsComponent implements ControlValueAccessor, OnInit {

  @Input() name: string;
  @Input() disabled = false;
  private propagateChange: Function;
  private onTouched: Function;

  private settingsService: SettingsService;
  selectedValue: any;

  constructor(settingsService: SettingsService) {
  this.settingsService = settingsService;
  }

  ngOnInit(): void {
  if (!this.name) {
  throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
  }
  }

  writeValue(obj: any): void {
  this.selectedValue = obj;
  }

  registerOnChange(fn: any): void {
  this.propagateChange = fn;
  }

  registerOnTouched(fn: any): void {
  this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
  this.disabled = isDisabled;
  }
  }

这是我的组件模板:

<select class="form-control"
  [disabled]="disabled"
  [(ngModel)]="selectedValue"
  (ngModelChange)="propagateChange($event)">
  <option value="">Select an option</option>
  <option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
  {{option.description}}
  </option>
  </select>

最佳答案

SAMPLE PLUNKER

我看到两个选项:

  1. 传播来自组件 FormControl 的错误至 <select> FormControl每当 <select> FormControl值(value)变化
  2. 从组件 FormControl 传播验证器至 <select> FormControl

以下变量可用:

  • selectModelNgModel<select>
  • formControlFormControl作为参数接收的组件

选项 1:传播错误

  ngAfterViewInit(): void {
    this.selectModel.control.valueChanges.subscribe(() => {
      this.selectModel.control.setErrors(this.formControl.errors);
    });
  }

选项 2:传播验证器

  ngAfterViewInit(): void {
    this.selectModel.control.setValidators(this.formControl.validator);
    this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator);
  }

两者之间的区别在于传播错误意味着已经有错误,而秒选项涉及第二次执行验证器。其中一些,例如异步验证器,可能执行起来成本太高。

传播所有属性?

没有传播所有属性的通用解决方案。各种属性由各种指令或其他方式设置,因此具有不同的生命周期,这意味着需要特定的处理。当前的解决方案涉及传播验证错误和验证器。那里有许多可用的属性。

请注意,您可能会从 FormControl 获得不同的状态更改例如订阅 FormControl.statusChanges() .这样你就可以得到控件是否为VALID , INVALID , DISABLEDPENDING (异步验证仍在运行)。

验证是如何进行的?

在底层,验证器是使用指令 (check the source code) 应用的。指令有 providers: [REQUIRED_VALIDATOR]这意味着自己的分层注入(inject)器用于注册该验证器实例。因此,根据应用于元素的属性,指令将在与目标元素关联的注入(inject)器上添加验证器实例。

接下来,这些验证器由 NgModel 检索和 FormControlDirective .

验证器和值访问器的检索方式如下:

  constructor(@Optional() @Host() parent: ControlContainer,
              @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
              @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
              @Optional() @Self() @Inject(NG_VALUE_ACCESSOR)

分别是:

  constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
              @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
              @Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
              valueAccessors: ControlValueAccessor[])

请注意 @Self()被使用,因此使用自己的注入(inject)器(指令被应用到的元素)来获取依赖项。

NgModelFormControlDirective有一个 FormControl 的实例它实际更新值并执行验证器。

因此,主要的交互点是 FormControl实例。

此外,所有验证器或值访问器都在应用它们的元素的注入(inject)器中注册。这意味着 parent 不应访问该注入(inject)器。因此,从当前组件访问 <select> 提供的注入(inject)器是一种不好的做法。 .

选项 1 的示例代码(可轻松替换为选项 2)

以下示例有两个验证器:一个是必需的,另一个是强制选项匹配“选项 3”的模式。

The PLUNKER

options.component.ts

import {AfterViewInit, Component, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import {SettingsService} from '../settings.service';

const OPTIONS_VALUE_ACCESSOR: any = {
  multi: true,
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => OptionsComponent)
};

@Component({
  providers: [OPTIONS_VALUE_ACCESSOR],
  selector: 'inf-select[name]',
  templateUrl: './options.component.html',
  styleUrls: ['./options.component.scss']
})
export class OptionsComponent implements ControlValueAccessor, OnInit, AfterViewInit {

  @ViewChild('selectModel') selectModel: NgModel;
  @Input() formControl: FormControl;

  @Input() name: string;
  @Input() disabled = false;

  private propagateChange: Function;
  private onTouched: Function;

  private settingsService: SettingsService;

  selectedValue: any;

  constructor(settingsService: SettingsService) {
    this.settingsService = settingsService;
  }

  ngOnInit(): void {
    if (!this.name) {
      throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
    }
  }

  ngAfterViewInit(): void {
    this.selectModel.control.valueChanges.subscribe(() => {
      this.selectModel.control.setErrors(this.formControl.errors);
    });
  }

  writeValue(obj: any): void {
    this.selectedValue = obj;
  }

  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

options.component.html

<select #selectModel="ngModel"
        class="form-control"
        [disabled]="disabled"
        [(ngModel)]="selectedValue"
        (ngModelChange)="propagateChange($event)">
  <option value="">Select an option</option>
  <option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
    {{option.description}}
  </option>
</select>

options.component.scss

:host {
  display: inline-block;
  border: 5px solid transparent;

  &.ng-invalid {
    border-color: purple;
  }

  select {
    border: 5px solid transparent;

    &.ng-invalid {
      border-color: red;
    }
  }
}

用法

定义 FormControl实例:

export class AppComponent implements OnInit {

  public control: FormControl;

  constructor() {
    this.control = new FormControl('', Validators.compose([Validators.pattern(/^option 3$/), Validators.required]));
  }
...

绑定(bind)FormControl组件的实例:

<inf-select name="myName" [formControl]="control"></inf-select>

虚拟设置服务

/**
 * TODO remove this class, added just to make injection work
 */
export class SettingsService {

  public getOption(name: string): [{ description: string }] {
    return [
      { description: 'option 1' },
      { description: 'option 2' },
      { description: 'option 3' },
      { description: 'option 4' },
      { description: 'option 5' },
    ];
  }
}

关于angular - 我可以在 Angular 2+ 中访问自定义 ControlValueAccessor 的 formControl 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44201298/

相关文章:

angular - 在 angular 2 上从 div 创建和下载 pdf

angular - 类型 'zip' 上不存在属性 'typeof Observable'。 Angular 6

javascript - 更新电子邮件验证状态而不重新加载页面

angular - 具有父级和子级路由的响应式(Reactive)表单

Angular Reactive 表单没有从隐藏字段中获取值(value)?

Angular 内容 child 访问和操作

angular - 如何将 www.materialpalette.com 中的调色板映射到 Angular 2 主题?

javascript - Angular Reactive Forms 验证的奇怪错误

angular - 响应式(Reactive)表单字段不使用 setValue 或 patchValue 更新

javascript - 如何在没有警告或ngModel的情况下制作下拉菜单?