Аксессор значения элемента управления: Пользовательские компоненты формы в Angular

Пользовательские компоненты, управляемые FormControl.

Смотрите эту и многие другие статьи на сайте lucaspaganini.com

Angular позволяет нам управлять входами формы с помощью FormsModule или ReactiveFormsModule. С их помощью вы можете привязать FormControl к вводу и управлять его значением.

<input type="text" [(ngModel)]="name" />
<input type="text" [formControl]="nameControl" />
Вход в полноэкранный режим Выход из полноэкранного режима

Но что, если вы создадите свой собственный компонент? Например, датапикер, звездный рейтинг или регекс-ввод. Можно ли привязать к нему FormControl?

<app-datepicker [(ngModel)]="date"></app-datepicker>
<app-datepicker [formControl]="dateControl"></app-datepicker>

<app-stars [(ngModel)]="stars"></app-stars>
<app-stars [formControl]="starsControl"></app-stars>

<app-regex [(ngModel)]="regex"></app-regex>
<app-regex [formControl]="regexControl"></app-regex>
Вход в полноэкранный режим Выход из полноэкранного режима

Встроенные входы и элементы управления формой

Ваша первая догадка, возможно, заключается в том, чтобы добавить @Input() в ваш компонент для получения formControl. Это сработает, но не при использовании formControlName или [(ngModel)].

На самом деле мы хотим повторно использовать ту же логику, которую Angular использует для привязки FormControl к собственным элементам ввода.

Если вы посмотрите на исходный код FormsModule, вы увидите директивы для собственных элементов ввода, реализующие интерфейс под названием ControlValueAccessor.

Этот интерфейс позволяет FormControl подключаться к компоненту.

Аксессор управляющего значения

Давайте создадим простой компонент ввода даты, чтобы проверить это на практике. Наш компонент должен реализовать интерфейс ControlValueAccessor.

@Component({
  selector: 'app-date-input',
  ...
})
export class DateInputComponent implements ControlValueAccessor {
  public readonly dayControl = new FormControl();
  public readonly monthControl = new FormControl();
  public readonly yearControl = new FormControl();
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Этот интерфейс определяет 4 метода:

  1. writeValue(value: T | null): void
  2. registerOnChange(onChange: (value: T | null) => void): void
  3. registerOnTouched(onTouched: () => void)
  4. setDisabledState(isDisabled: boolean): void

registerOnChange получает функцию обратного вызова, которую нужно вызывать при изменении значения. Аналогично, registerOnTouched получает функцию обратного вызова, которую нужно вызывать при касании ввода.

private _onChange = (value: Date | null) => undefined;
public registerOnChange(fn: (value: Date | null) => void): void {
  this._onChange = fn;
}

private _onTouched = () => undefined;
public registerOnTouched(fn: () => void): void {
  this._onTouched = fn;
}

public ngOnInit(): void {
  combineLatest([
    this.dayControl.valueChanges,
    this.monthControl.valueChanges,
    this.yearControl.valueChanges,
  ]).subscribe(([day, month, year]) => {
    const fieldsAreValid =
      this.yearControl.valid &&
      this.monthControl.valid &&
      this.dayControl.valid;
    const value = fieldsAreValid ? new Date(year, month - 1, day) : null;

    this._onChange(value);
    this._onTouched();
  });
}
Вход в полноэкранный режим Выход из полноэкранного режима

writeValue вызывается, когда значение FormControl изменяется программно, например, когда вы вызываете FormControl.setValue(x). Он может принимать что угодно, но если вы используете его правильно, он должен принимать только T (T = Date в нашем случае) или null.

public writeValue(value: Date | null): void {
    value = value ?? new Date();

    const day = value.getDate();
    const month = value.getMonth() + 1;
    const year = value.getFullYear();

    this.dayControl.setValue(day);
    this.monthControl.setValue(month);
    this.yearControl.setValue(year);
  }
Вход в полноэкранный режим Выйти из полноэкранного режима

Последний метод является необязательным. setDisabledState() вызывается, когда статус FormControl изменяется на состояние disabled или из состояния disabled.

Этот метод получает единственный аргумент, указывающий, является ли новое состояние отключенным. Если он был отключен, а теперь включен, то метод вызывается с false. Если состояние было включено, а теперь отключено, то вызывается true.

public setDisabledState(isDisabled: boolean): void {
  if (isDisabled) {
    this.dayControl.disable();
    this.monthControl.disable();
    this.yearControl.disable();
  } else {
    this.dayControl.enable();
    this.monthControl.enable();
    this.yearControl.enable();
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Предоставление NG_VALUE_ACCESSOR.

Последний шаг для того, чтобы все заработало, — сообщить Angular, что наш компонент готов подключаться к FormControls.

Все классы, реализующие интерфейс ControlValueAccessor, получают токен NG_VALUE_ACCESSOR. Angular использует этот токен для захвата ControlValueAccessor и подключения к нему FormControl.

Итак, мы предоставим наш компонент в этом токене, и Angular будет использовать его для подключения к FormControl.

Кстати, поскольку мы предоставляем наш компонент до его объявления, нам нужно будет использовать функцию Angular forwardRef(), чтобы это сработало.

@Component({
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DateInputComponent),
      multi: true,
    },
  ],
  ...
})
export class DateInputComponent implements ControlValueAccessor { ... }
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Теперь все должно работать. Вы можете поиграть с кодом в этом репозитории.

Есть еще одна вещь, которую я хотел бы сделать с нашим пользовательским вводом даты: Я хочу, чтобы он проверял вводимые данные. 31 февраля не является допустимой датой, и мы не должны ее принимать.

Кроме того, я хочу принимать только рабочие дни. Для этого нам понадобится синхронная проверка, чтобы убедиться, что это будний день, и асинхронная проверка, чтобы обратиться к API и убедиться, что это не праздничный день.

Мы сделаем это в другой статье.

Хорошего дня, и до скорой встречи!

Ссылки

  1. Репозиторий GitHub

Оцените статью
Procodings.ru
Добавить комментарий