Как работает ControlValueAccessor?
ControlValueAccessor в Angular — это интерфейс, позволяющий создать компонент, который может взаимодействовать с Angular Forms API (Reactive Forms или Template-Driven Forms) так, как это делают встроенные элементы управления (input, select, checkbox и др.). Это ключ к реализации кастомных компонентов ввода, полностью интегрированных с формами Angular.
1. Назначение
Без реализации ControlValueAccessor Angular не сможет:
-
привязать значение формы к компоненту;
-
уведомить форму об изменениях в значении;
-
управлять состоянием (disabled, touched);
-
синхронизировать ошибки валидации.
С помощью ControlValueAccessor вы говорите Angular: “Вот как мой компонент работает с моделью формы”.
2. Интерфейс ControlValueAccessor
Интерфейс содержит 4 метода:
interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
3. Реализация пошагово
3.1 Создание кастомного компонента
@Component({
selector: 'app-custom-input',
template: \`<input \[value\]="value" (input)="onInput($event)" (blur)="onTouched()" />\`,
providers: \[{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}\]
})
export class CustomInputComponent implements ControlValueAccessor {
value: string = '';
private onChange = (\_: any) => {};
private onTouched = () => {};
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
// Реализуется при необходимости
}
onInput(event: Event) {
const newValue = (event.target as HTMLInputElement).value;
this.value = newValue;
this.onChange(newValue);
}
}
4. Пояснение к методам
-
writeValue(value: any): вызывается, когда Angular хочет передать значение компоненту (например, при инициализации формы).
-
registerOnChange(fn: Function): Angular передаёт функцию, которую необходимо вызывать при изменении значения в компоненте.
-
registerOnTouched(fn: Function): Angular передаёт функцию, которую нужно вызвать при потере фокуса (blur) для отслеживания touched-состояния.
-
setDisabledState?(isDisabled: boolean): необязательный метод, вызывается для активации/деактивации компонента. Должен применяться к disabled свойствам HTML-элементов.
5. Использование в форме
Reactive Forms:
form = new FormGroup({
username: new FormControl('')
});
<form \[formGroup\]="form">
<app-custom-input formControlName="username"></app-custom-input>
</form>
6. Множественные NG_VALUE_ACCESSOR
Так как в приложении могут быть несколько ControlValueAccessor, в providers нужно указать multi: true.
providers: \[{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}\]
7. Обработка disabled
Если компонент использует внутренний <input>, то метод setDisabledState() должен его отключать:
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
<input \[disabled\]="disabled" />
8. Интеграция с валидацией
Если компонент должен поддерживать ngModel, formControl, formControlName — реализация ControlValueAccessor обязательна.
Для поддержки кастомной валидации добавляется Validator интерфейс и NG_VALIDATORS.
9. Использование Template-Driven Forms (ngModel)
<app-custom-input \[(ngModel)\]="name"></app-custom-input>
Angular вызывает writeValue, registerOnChange, registerOnTouched при связывании с ngModel.
10. Поддержка FormControl.setValue и patchValue
Метод writeValue() вызывается при любых операциях, изменяющих значение в форме.
this.form.get('username')?.setValue('admin');
Вызовет:
writeValue('admin')
11. Множественные компоненты: valueAccessor выбирается Angular
Если несколько директив/компонентов претендуют на роль ControlValueAccessor в одном и том же DOM-элементе, Angular выбрасывает ошибку NG0100: More than one custom value accessor.
Решение — оставить только один NG_VALUE_ACCESSOR для элемента или использовать useExisting с forwardRef.
12. Поддержка touched, dirty, pristine
-
registerOnTouched() нужно вызывать при blur событиях;
-
onChange() — при любом изменении значения.
Это позволяет Angular корректно отслеживать состояние формы.
13. ControlValueAccessor + ngModelOptions
Если используется ngModelOptions, Angular вызывает onChange в соответствии с указанными параметрами (например, { updateOn: 'blur' }).
14. Пример для checkbox
<input type="checkbox" \[checked\]="value" (change)="toggle($event)" (blur)="onTouched()" />
value = false;
toggle(event: Event) {
const checked = (event.target as HTMLInputElement).checked;
this.value = checked;
this.onChange(checked);
}
15. Пример для select-мульти
<select multiple (change)="onSelect($event)" \[value\]="value">
<option \*ngFor="let item of options" \[value\]="item">{{ item }}</option>
</select>
onSelect(event: Event) {
const selectedOptions = Array.from((event.target as HTMLSelectElement).selectedOptions).map(o => o.value);
this.value = selectedOptions;
this.onChange(selectedOptions);
}
16. Проверка состояния
Можно в шаблоне использовать ngModel, formControl, formControlName, formGroup и получать доступ к валидности, touched-состоянию и пр.:
<app-custom-input formControlName="email"></app-custom-input>
<div \*ngIf="form.get('email')?.touched && form.get('email')?.invalid">
Некорректный email
</div>
ControlValueAccessor — это низкоуровневый API, дающий разработчику контроль над тем, как его компонент связывается с Angular Forms. Он делает возможным расширение функциональности форм Angular за счёт собственных UI-компонентов, сохраняя при этом преимущества реактивного подхода, валидации, привязки состояний и интеграции с форм-менеджментом.