Модуль 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(); // і що контрол чіпали
}
}
Валідація власного контролу
// компонент може ще й сам себе валідувати
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 працює з одним.
CVA — не обовʼязковий пункт таблиці, це «плюс до карми». Якщо часу мало, зроби один простий контрол (наприклад, вибір статусу бейджами) — цього досить, щоб показати розуміння. Якщо часу немає зовсім, пропусти й повернись пізніше: решта пунктів модуля важливіші.