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

时间:2017-05-26 12:11:20

标签: angular angular-reactive-forms

我想在Angular 2+中使用ControlValueAccessor接口创建一个自定义表单元素。这个元素将是<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>

4 个答案:

答案 0 :(得分:7)

SAMPLE PLUNKER

我看到两个选项:

  1. 每当FormControl <select>值更改
  2. 时,都会将组件FormControl中的错误传播到<select> FormControl
  3. 将验证工具从FormControl组件传播到<select> FormControl
  4. 以下变量可用:

    • selectModelNgModel
    • <select>
    • 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);
      }
    

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

    宣传所有媒体资源?

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

    请注意,您可以通过订阅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实例。

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

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

    Dummy SettingsService

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

答案 1 :(得分: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
       }
    }
}

答案 2 :(得分:0)

如果实现验证(Validator / NG_VALIDATORS),则AbstractControl会很早传递到验证函数中。您可以将其藏起来。

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

答案 3 :(得分:0)

这里是一个示例,显示了如何获取(和重用)基础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);
  }
}