Angular IDPStrong Junior
Модулі / Форми / ControlValueAccessor
🔥 0

Модуль 6 · урок 10 з 12

ControlValueAccessor

ControlValueAccessor — це інтерфейс, який робить твій компонент повноцінним елементом форми. Після нього власний віджет можна писати в шаблоні як звичайний formControlName, і Angular ставитиметься до нього як до <input>.

Навіщо це

// ❌ без CVA: свій компонент живе поза формою
<app-status-picker
  [value]="form.controls.status.value"
  (valueChange)="form.controls.status.setValue($event)" />

// ✅ з CVA: він частина форми
<app-status-picker formControlName="status" />

Різниця не лише в довжині рядка. У другому варіанті компонент автоматично отримує валідацію, стани touched і dirty, реакцію на disable(), reset() і patchValue(). У першому все це доводиться робити руками.

Чотири методи

@Component({
  selector: 'app-status-picker',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => StatusPicker),   // клас ще не оголошено — звідси forwardRef
    multi: true,
  }],
  template: `
    @for (option of options; track option.value) {
      <button type="button"
              [class.is-active]="value() === option.value"
              [disabled]="disabled()"
              (click)="pick(option.value)">
        {{ option.label }}
      </button>
    }
  `,
})
export class StatusPicker implements ControlValueAccessor {
  protected readonly value = signal<TaskStatus | null>(null);
  protected readonly disabled = signal(false);

  private onChange: (v: TaskStatus | null) => void = () => {};
  private onTouched: () => void = () => {};

  // 1. форма → компонент: сюди приходить значення при patchValue, reset тощо
  writeValue(value: TaskStatus | null): void {
    this.value.set(value);
  }

  // 2. компонент → форма: зберігаємо колбек, який треба кликати при зміні
  registerOnChange(fn: (v: TaskStatus | null) => void): void {
    this.onChange = fn;
  }

  // 3. те саме для «користувач залишив контрол»
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  // 4. реакція на disable()/enable() з боку форми
  setDisabledState(isDisabled: boolean): void {
    this.disabled.set(isDisabled);
  }

  protected pick(next: TaskStatus): void {
    if (this.disabled()) return;
    this.value.set(next);
    this.onChange(next);   // повідомити форму
    this.onTouched();      // і що контрол чіпали
  }
}
FORMCONTROL значення й статус валідатори ТВІЙ КОМПОНЕНТ своя розмітка й поведінка нічого не знає про форму writeValue — значення вниз onChange / onTouched — зміни вгору Це той самий однонаправлений потік із модуля 2, тільки описаний інтерфейсом, який розуміє Angular.
CVA — це контракт із чотирьох методів: два для отримання даних від форми, два для повідомлення форми про зміни.

Валідація власного контролу

// компонент може ще й сам себе валідувати
providers: [
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StatusPicker), multi: true },
  { provide: NG_VALIDATORS, useExisting: forwardRef(() => StatusPicker), multi: true },
]

validate(control: AbstractControl): ValidationErrors | null {
  return control.value ? null : { statusRequired: true };
}

Робити це варто лише тоді, коли правило невіддільне від самого контролу. Якщо ж обовʼязковість залежить від контексту використання — краще лишити Validators.required на боці форми.

Коли CVA виправданий

Варто
  • Компонент використовується у кількох формах.
  • Це справді «поле»: селект, перемикач, редактор тегів, вибір дати.
  • Потрібні стани форми: touched, disabled, reset.
  • Хочеш, щоб він працював із formControlName як рідний.
Не варто
  • Компонент використовується рівно в одному місці.
  • Це не поле, а секція форми — тоді просто вкладена FormGroup.
  • Досить input і output.
  • Він змінює кілька значень одразу — CVA працює з одним.
Для IDP цього достатньо

CVA — не обовʼязковий пункт таблиці, це «плюс до карми». Якщо часу мало, зроби один простий контрол (наприклад, вибір статусу бейджами) — цього досить, щоб показати розуміння. Якщо часу немає зовсім, пропусти й повернись пізніше: решта пунктів модуля важливіші.

≈ 45 хв · +25 XP за урок