【问题标题】:Can I access to formControl of my custom ControlValueAccessor in Angular 2+?我可以在 Angular 2+ 中访问我的自定义 ControlValueAccessor 的 formControl 吗?
【发布时间】:2017-10-27 07:57:08
【问题描述】:

我想在 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;
  }
  }

这是我的组件模板:

<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>

【问题讨论】:

  • 你能用 plunker 复制它吗?

标签: angular angular-reactive-forms


【解决方案1】:

SAMPLE PLUNKER

我看到两个选项:

  1. 每当&lt;select&gt; FormControl 值发生变化时,将错误从组件FormControl 传播到&lt;select&gt; FormControl
  2. 将验证器从组件FormControl 传播到&lt;select&gt; FormControl

以下变量可用:

  • selectModelNgModel&lt;select&gt;
  • formControl 是作为参数接收的组件的FormControl

选项 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);
  }

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

传播所有属性?

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

请注意,通过订阅FormControl.statusChanges(),您可能会从FormControl 实例中获得不同的状态更改。这样就可以知道控件是VALIDINVALIDDISABLED还是PENDING(异步验证还在运行中)。

验证的工作原理是什么?

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

接下来,这些验证器由NgModelFormControlDirective 检索。

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

  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() 被使用,因此使用自己的注入器(应用指令的元素的)来获取依赖关系。

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

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

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

选项 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]));
  }
...

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' },
    ];
  }
}

【讨论】:

  • 您好!谢谢您的回答。但是,当通过 FormBuilder 构造表单时,如何访问FormControl?在您的示例中,组件将以这种方式调用:&lt;inf-select formControlName="someControl"&gt;&lt;/inf-select&gt;.
  • @SlavaFominII 假设您使用FormBuilder 创建了一个FormGroup。然后您可以通过formGroup.controls['someControl']formGroup.get('someControl') 获得控制权。
  • 是的,我知道,谢谢。我在这里澄清了我的问题:stackoverflow.com/questions/44731894/…
  • 将 *ngFor 与方法 (ngFor="let option of settingsService.getOption(name)") 一起使用是一种非常糟糕的做法。即使返回的数组(显然)相同,它也会被不断调用。
  • @adripanico 好点!该实现是问题的一部分,我故意将所有内容保留在适当的位置,以便用户通过比较两者来轻松同化。所以我把所有东西都删掉了,以便看起来几乎一样。
【解决方案2】:

在我看来,这是在基于 ControlValueAccessor 的组件中访问 FormControl 的最简洁的解决方案。解决方案基于提到的here in Angular Material documentation

// parent component template
<my-text-input formControlName="name"></my-text-input>
@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
       }
    }
}

【讨论】:

  • 对我来说不起作用,您能否在 stackblitz 中提供一个工作示例?
【解决方案3】:

如果您实施验证 (Validator / NG_VALIDATORS),AbstractControl 会很早就传递到您的验证函数中。你可以把它藏起来。

  validate(c: AbstractControl): ValidationErrors {
    this.myControl = c;

【讨论】:

  • 这是正确的。 OP 要求在 FromGroup 传播它们时尽快捕获 FormControl 错误。如果你使用 .sertErrors();来自 FormGroup,它不会触发 validate() 函数。
【解决方案4】:

这是一个示例,展示了如何获取(和重用)底层 FormControl 和底层 ControlValueAccessor。

这在包装组件(如输入)时很有用,因为您可以重复使用 Angular 创建的现有 FormControl 和 ControlValueAccessor,从而避免重新实现它。

@Component({
  selector: 'resettable-input',
  template: `
     <input type="text" [formControl]="control">
     <button (click)="clearInput()">clear</button>
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: ResettableInputComponent,
    multi: true
  }]
})
export class ResettableInputComponent implements ControlValueAccessor {

  @ViewChild(FormControlDirective, {static: true}) formControlDirective: FormControlDirective;
  @Input() formControl: FormControl;

  @Input() formControlName: string;

  // get hold of FormControl instance no matter formControl or formControlName is given.
  // If formControlName is given, then this.controlContainer.control is the parent FormGroup (or FormArray) instance.
  get control() {
    return this.formControl || this.controlContainer.control.get(this.formControlName);
  }

  constructor(private controlContainer: ControlContainer) { }

  clearInput() {
    this.control.setValue('');
  }

  registerOnTouched(fn: any): void {
    this.formControlDirective.valueAccessor.registerOnTouched(fn);
  }

  registerOnChange(fn: any): void {
    this.formControlDirective.valueAccessor.registerOnChange(fn);
  }

  writeValue(obj: any): void {
    this.formControlDirective.valueAccessor.writeValue(obj);
  }

  setDisabledState(isDisabled: boolean): void {
    this.formControlDirective.valueAccessor.setDisabledState(isDisabled);
  }
}

【讨论】:

    猜你喜欢
    • 2020-04-18
    • 1970-01-01
    • 1970-01-01
    • 2017-11-27
    • 2017-08-17
    • 1970-01-01
    • 2021-08-04
    • 2018-03-17
    • 1970-01-01
    相关资源
    最近更新 更多