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

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

Ответ 1

ОБРАЗЕЦ ОБРАЗЦОВ

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

  1. Распространять ошибки из компонента FormControl на <select> FormControl всякий раз, когда FormControl значение <select> FormControl
  2. Распространять валидаторы из компонента FormControl в <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 (асинхронная проверка еще выполняется).

Как валидация работает под капотом?

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

ПЛАНКЕР

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