Как использовать ControlValueAccessor для кастомных полей ввода?angular-62

ControlValueAccessor (CVA) — это интерфейс, который позволяет создавать пользовательские элементы управления, полностью интегрируемые с Angular Forms (как реактивными, так и шаблонными). Это мощный инструмент для создания переиспользуемых компонентов ввода.

Зачем нужен ControlValueAccessor?

  1. Интеграция с Angular Forms - позволяет кастомным компонентам работать с formControl, formControlName
  2. Поддержка валидации - автоматическая работа с валидаторами
  3. Согласованное API - единый способ работы со всеми элементами ввода
  4. Двустороннее связывание - поддержка [(ngModel)]

Реализация ControlValueAccessor

Для создания кастомного элемента управления нужно:

  1. Реализовать интерфейс ControlValueAccessor
  2. Зарегистрировать провайдер NG_VALUE_ACCESSOR
  3. Обеспечить взаимодействие с родительской формой

Базовый пример: Кастомный ввод

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

@Component({
  selector: 'app-custom-input',
  template: `
    <input
      [type]="type"
      [value]="value"
      [disabled]="disabled"
      (input)="onInput($event)"
      (blur)="onBlur()"
    >
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
export class CustomInputComponent implements ControlValueAccessor {
  @Input() type = 'text';
  value: string = '';
  disabled = false;

  // Функции для взаимодействия с Angular Forms
  onChange: any = () => {};
  onTouched: any = () => {};

  // Вызывается при изменении значения в форме
  writeValue(value: string): void {
    this.value = value || '';
  }

  // Регистрируем callback на изменения
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  // Регистрируем callback на "touched"
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  // Устанавливаем disabled состояние
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  // Обработчик ввода
  onInput(event: Event): void {
    const newValue = (event.target as HTMLInputElement).value;
    this.value = newValue;
    this.onChange(newValue); // Сообщаем Angular Forms о изменении
  }

  // Обработчик blur
  onBlur(): void {
    this.onTouched(); // Сообщаем Angular Forms о "touched"
  }
}

Использование в шаблоне

<!-- С реактивными формами -->
<form [formGroup]="form">
  <app-custom-input formControlName="username"></app-custom-input>
</form>

<!-- С ngModel -->
<app-custom-input [(ngModel)]="userName"></app-custom-input>

Расширенный пример: Кастомный чекбокс

@Component({
  selector: 'app-toggle',
  template: `
    <div
      (click)="toggle()"
      [class.checked]="value"
      [class.disabled]="disabled"
    >
      {{ value ? 'On' : 'Off' }}
    </div>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ToggleComponent),
      multi: true
    }
  ]
})
export class ToggleComponent implements ControlValueAccessor {
  value = false;
  disabled = false;

  onChange: any = () => {};
  onTouched: any = () => {};

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

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

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

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

  toggle(): void {
    if (this.disabled) return;

    this.value = !this.value;
    this.onChange(this.value);
    this.onTouched();
  }
}

Поддержка валидации

Кастомные элементы с CVA автоматически поддерживают валидацию:

this.form = this.fb.group({
  email: ['', [Validators.required, Validators.email]],
  notifications: [false, Validators.requiredTrue]
});

Составные элементы управления

Для сложных элементов (например, адреса с несколькими полями):

@Component({
  selector: 'app-address-input',
  template: `
    <input [(ngModel)]="address.street" (ngModelChange)="onChange()">
    <input [(ngModel)]="address.city" (ngModelChange)="onChange()">
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => AddressInputComponent),
      multi: true
    }
  ]
})
export class AddressInputComponent implements ControlValueAccessor {
  address = { street: '', city: '' };

  writeValue(value: any): void {
    this.address = value || { street: '', city: '' };
  }

  onChange(): void {
    this.onChangeCallback(this.address);
  }

  // ... остальные методы CVA
}

Оптимизация производительности

  1. Используйте ChangeDetectionStrategy.OnPush
  2. Минимизируйте вызовы onChange (можно добавить debounce)
  3. Для сложных элементов избегайте глубокого копирования значений

Резюмируем

  1. ControlValueAccessor - мост между кастомными компонентами и Angular Forms
  2. Обязательные методы:
    • writeValue - для получения значения из формы
    • registerOnChange - регистрация callback на изменения
    • registerOnTouched - регистрация callback на touched
    • setDisabledState - обработка disabled состояния
  3. Регистрация через NG_VALUE_ACCESSOR провайдер
  4. Поддержка всех возможностей Angular Forms:
    • Реактивные формы и ngModel
    • Валидация
    • Состояния touched/pristine/dirty
  5. Применение для:
    • Кастомных элементов ввода
    • Составных компонентов
    • Специализированных UI-контролов

Правильная реализация CVA позволяет создавать мощные переиспользуемые компоненты, которые идеально интегрируются в экосистему Angular Forms.