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

SaW*_*aWo 18 angular angular-reactive-forms

我想在Angular 2+中使用ControlValueAccessor接口创建一个自定义表单元素.这个元素将是一个包装器<select>.是否可以将formControl属性传播到包装元素?在我的情况下,验证状态不会传播到嵌套选择,如您在附加的屏幕截图中看到的那样.

在此输入图像描述

我的组件可用如下:

  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;
  }
  }
Run Code Online (Sandbox Code Playgroud)

这是我的组件模板:

<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>
Run Code Online (Sandbox Code Playgroud)

and*_*eim 9

SAMPLE PLUNKER

我看到两个选择:

  1. 将值中的错误传播FormControl到值更改时<select> FormControl<select> FormControl
  2. 将组件中的验证器传播FormControl<select> FormControl

以下变量可用:

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

选项1:传播错误

  ngAfterViewInit(): void {
    this.selectModel.control.valueChanges.subscribe(() => {
      this.selectModel.control.setErrors(this.formControl.errors);
    });
  }
Run Code Online (Sandbox Code Playgroud)

选项2:传播验证者

  ngAfterViewInit(): void {
    this.selectModel.control.setValidators(this.formControl.validator);
    this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator);
  }
Run Code Online (Sandbox Code Playgroud)

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

传播所有属性?

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

请注意,您可能FormControl通过订阅从实例获得不同的状态更改FormControl.statusChanges().这样,您就可以得到控制是VALID,INVALID,DISABLED或者PENDING(异步验证仍在运行).

验证如何在引擎盖下工作?

在引擎盖下,使用指令应用验证器(检查源代码).指令具有providers: [REQUIRED_VALIDATOR]这意味着自己的分层注入器用于注册该验证器实例.因此,根据应用于元素的属性,指令将在与目标元素关联的注入器上添加验证器实例.

接下来,这些验证器由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)
Run Code Online (Sandbox Code Playgroud)

分别为:

  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[])
Run Code Online (Sandbox Code Playgroud)

注意@Self()使用,因此使用自己的注入器(应用指令的元素)以获得依赖性.

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

因此,与之交互的要点是FormControl实例.

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

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

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

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

用法

定义FormControl实例:

export class AppComponent implements OnInit {

  public control: FormControl;

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

FormControl实例绑定到组件:

<inf-select name="myName" [formControl]="control"></inf-select>
Run Code Online (Sandbox Code Playgroud)

虚拟设置服务

/**
 * 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' },
    ];
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 在方法中使用* ngFor是非常不好的做法(```ngFor =“ settingsService.getOption(name)” let选项“)。即使返回的数组(显然)是相同的,也会不断调用它。 (2认同)
  • @adripanico好点!该实现是问题的一部分,我特意将所有内容保留在原处,以便用户通过比较两者来轻松地吸收它们。因此,我对所有内容进行了打桩以使其看起来几乎相同。 (2认同)

Rui*_*ues 8

在我看来,这是在基础组件中访问 FormControl 的最干净的解决方案ControlValueAccessor解决方案基于 Angular Material 文档中提到的内容。

// parent component template
<my-text-input formControlName="name"></my-text-input>
Run Code Online (Sandbox Code Playgroud)
@Component({
  selector: 'my-text-input',
  template: '<input
    type="text"
    [value]="value"
  />',
})
export class MyComponent implements AfterViewInit, ControlValueAccessor  {

  // Here is missing standard stuff to implement ControlValueAccessor interface

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (ngControl != null) {
      // Setting the value accessor directly (instead of using
      // the providers) to avoid running into a circular import.
      ngControl.valueAccessor = this;
    }
  }

    ngAfterContentInit(): void {
       const control = this.ngControl && this.ngControl.control;
       if (control) {
          // FormControl should be available here
       }
    }
}
Run Code Online (Sandbox Code Playgroud)