Могу ли я получить доступ к formControl моего пользовательского ControlValueAccessor в Angular 2+?

Я хотел бы создать пользовательский элемент формы с интерфейсом ControlValueAccessor в Angular 2+. Этот элемент будет оберткой над <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>

1 ответов


ОБРАЗЕЦ PLUNKER

я вижу два варианта:

  1. распространять ошибки из компонента FormControl to <select> FormControl, когда <select> FormControl изменения стоимости
  2. распространить валидаторы из компонента FormControl to <select> FormControl

ниже доступны следующие переменные:

  • selectModel - это NgModel на <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 экземпляр подписавшись на FormControl.statusChanges(). Таким образом, вы можете получить, является ли элемент управления VALID, INVALID, DISABLED или PENDING (асинхронная проверка все еще выполняется).

как проверка работает под капотом?

под капотом валидаторы применяются директивы using ( проверьте исходный код). В директивы 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)

и соответственно:

  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() используется, поэтому собственный инжектор (из элемент, к которому применяется директива) используется для получения зависимостей.

NgModel и FormControlDirective экземпляр FormControl которые фактически обновляют значение и выполняют валидаторы.

поэтому основной момент для взаимодействия с FormControl экземпляра.

также все валидаторы или аксессоры значения зарегистрированы в инжекторе элемента, к которому они применяются. Это означает, что родитель должен нет доступа к инжектору. Так что было бы плохой практикой для доступа из текущего компонента инжектора, предоставляемого <select>.

пример кода для варианта 1 (легко заменяется вариантом 2)

следующий образец имеет два валидатора: один, который требуется, и другой, который является шаблоном, который заставляет параметр соответствовать "варианту 3".

в PLUNKER

параметры.деталь.ТС

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

параметры.деталь.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>

параметры.деталь.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>

Манекен 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' },
    ];
  }
}