Создание custom form field control (ControlValueAccessor)

  1. ControlValueAccessor - связующее звено между Angular forms API и нативным DOM элементом
  2. Чтобы Angular знал об новом field control необходимо зарегистрировать NG_VALUE_ACCESSOR провайдер.

Интерфейс ControlValueAccessor

В компоненте должны реализовываться 3 обязательных метода и 1 один необязательный:

export interface ControlValueAccessor {
    /**
     * Записать значение в компонент (из ts в html), основная функция
     */
    writeValue(obj: any): void;
    /**
     * Обработать значение из компонента (из html в ts), основная функция fn
     */
    registerOnChange(fn: any): void;
    /**
     * Обработать, когда потрогали поле (потеря фокуса)
     */
    registerOnTouched(fn: any): void;
    /**
     * Обработка недоступности
     */
    setDisabledState?(isDisabled: boolean): void;
}

Регистрация NG_VALUE_ACCESSOR провайдера

Используется мультипровайдер с токеном NG_VALUE_ACCESSOR.

const NG_VALUE_ACCESSOR: InjectionToken<ControlValueAccessor>;

Функция forwardRef необходима потому, что в провайдере идёт обращение к классу, который еще не определен (объявлен ниже). Иначе ошибка (ERROR in : Cannot instantiate cyclic dependency! NgControl)

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  ...
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => TyapkComponent),
    multi: true
  }]
}) export class TyapkComponent implements ControlValueAccessor {}

Custom control

Миниальный

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-quantity-input',
  template: `
    <button 
      (click)="updateValue(+value - 10)" 
      [disabled]="disabled">--</button>
    <button 
      (click)="updateValue(+value - 1)" 
      [disabled]="disabled">-</button>
    <input
      [ngModel]="value"
      (ngModelChange)="updateValue($event)"
      [disabled]="disabled"
      type="number"
    />
    <button 
      (click)="updateValue(+value + 1)" 
      [disabled]="disabled">+</button>
    <button 
      (click)="updateValue(+value + 10)" 
      [disabled]="disabled">++</button>
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => QuantityInputComponent),
    multi: true,
  }],
})
export class QuantityInputComponent implements ControlValueAccessor {
  value = 0;
  disabled = false;
  private onChange = (value: any) => {};
  private onTouched = () => {};

  registerOnChange(fn: any) {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => {}): void {
    this.onTouched = fn;
  }

  writeValue(outsideValue: number) {
    // получить из Forms API
    this.value = outsideValue;
  }

  setDisabledState(isDisabled: boolean) {
    this.disabled = isDisabled;
  }

  updateValue(insideValue: number) {
    this.value = insideValue; // html
    this.onChange(insideValue); // уведомить Forms API
    this.onTouched();
  }
}

stackblitz demo

Супер простой компонент переключателя

@Component({
  selector: 'toggle',
  template: `{{ value }}`,
  styles: [`
    :host {
      display: block;
      height: 20px;
      width: 285px;
      cursor: pointer;
      border: 1px solid #ccc;
      border-radius: 4px;
      padding: 5px;
      text-align: center;
    }

    :host(.disabled) {
      opacity: 0.35;
      pointer-events: none;
    }  
  `],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => ToggleComponent),
    multi: true
  }]
})
export class ToggleComponent implements ControlValueAccessor {
  value: boolean;
  disabled: boolean;
  onChange = (value: boolean) => {};
  onTouched = () => {};

  registerOnChange(fn) {
    this.onChange = fn;
  }

  registerOnTouched(fn) {
    this.onTouched = fn;
  }

  @HostBinding('class.disabled')
  get isDisabled() {
    return this.disabled;
  }

  @HostListener('click') 
  click() {
    this.writeValue(!this.value);
  }

  writeValue(value: boolean) {
    this.value = value;
    this.onChange(this.value);
  }

  setDisabledState(disabled: boolean) {
    this.disabled = disabled;
  }
}

Классика

Рассказ автора Angular форм об имплементации ControlValueAccessor:

  • ControlValueAccessor - 12:25
  • Nested Forms - 25:23

Похожие записи

Отладка Angular в VS Code

Настройка Visual Studio Code используя расширение Debugger for Chrome для отладки Angular приложений
03 декабря 2017 г. в Angular

@Directive v/s @Component in Angular

Компоненты создают DOM элементы и добавляют к ним поведение, а директивы только добавляют поведение к существующим DOM элементам

13 августа 2018 г. в Angular

RxJs Subjects

Выдержки из доклада Андрея Алексеева (Tinkoff) про RxJs (Subject, Behaviour Subject, Replay Subject, Async Subject). Применение в Angular.

Angular Let Directive

*ngIf не отображает содержимое в falsy случаях (0, null, undefined) на async pipe, в пакете @rx-angular/template предлагается решение