Angular forms чудово працюють зі стандартним HTML. input, textarea та select легко інтегруються в Reactive Forms без додаткових зусиль. Фреймворк розуміє їхні події, значення та стани.
Але сучасні додатки рідко обмежуються лише стандартними елементами. Вам може знадобитися віджет зірочкового рейтингу, складний селектор дати або кастомний color picker. Якщо ви додасте один із них у form group, Angular сприйматиме його як «мертвий» HTML. patchValue нічого не робитиме. Валідатори його ігноруватимуть. Форма не знатиме, коли користувач взаємодіє з елементом керування, а form.disable() залишить кастомний віджет повністю інтерактивним.
Саме цю проблему і покликаний вирішити ControlValueAccessor.
Що насправді робить ControlValueAccessor
ControlValueAccessor — це контракт, який перетворює кастомний компонент на повноцінного «громадянина» форм. Він виступає перекладачем між Angular Forms API та вашим власним UI. Щойно ви правильно його реалізуєте, ваш компонент стане невідрізним від нативного інпуту з точки зору форми. Він зможе отримувати значення, передавати зміни, повідомляти про взаємодію (touches) та дотримуватися стану disabled, як і вбудований елемент.
Інтерфейс вимагає чотирьох специфічних методів. Кожен із них відповідає за окремий напрямок комунікації.
writeValue: Від форми до компонента
writeValue(obj) — це вхідна смуга. Щоразу, коли модель форми оновлюється і потрібно передати нове значення у ваш UI, Angular викликає цей метод. Якщо ви викликаєте patchValue({ rating: 4 }) у form group, це значення 4 потрапляє всередину вашого компонента через writeValue. Якщо ви скидаєте форму, writeValue отримує нове початкове значення або null. Ваше завдання в цьому методі — взяти ці вхідні дані та відобразити їх у внутрішньому стані вашого компонента. Якщо ви створюєте color picker, writeValue отримує hex-рядок на кшталт #ff4400, і ви повинні оновити свій view, щоб показати цей колір як обраний.
Тут є один практичний нюанс. Angular може викликати writeValue ще до того, як ваш view буде повністю ініціалізовано, особливо всередині динамічно рендерених компонентів, діалогових вікон або інтерфейсів із вкладками. Якщо ваш компонент спробує звернутися до DOM або дочірніх компонентів занадто рано, ви можете отримати помилки під час виконання (runtime errors). Надійний патерн — зберігати значення в локальній властивості та застосовувати його після ініціалізації view або додавати перевірки на наявність дочірніх посилань. Ніколи не припускайте, що writeValue спрацьовує лише тоді, коли ваш шаблон стабільний.
registerOnChange: Від компонента до форми
registerOnChange(fn) налаштовує вихідну смугу. Angular передає вам функцію зворотного виклику (callback), і ви повинні зберігати на неї посилання. Щоразу, коли користувач змінює значення всередині вашого компонента, ви викликаєте цю функцію з новим значенням. У компоненті зірочкового рейтингу, коли користувач натискає на третю зірку, ви викликаєте збережений callback із значенням 3. Цей виклик повертається в FormControl, оновлює модель, запускає будь-які підписки на valueChanges і повторно запускає валідатори.
Пропуск цього кроку — найпоширеніший спосіб непомітно зламати форму. Віджет може виглядати робочим: користувач бачить, як загоряються зірки, змінюються кольори або заповнюються дати. Але модель форми ніколи не оновлюється. Валідатори продовжують перевіряти застарілі дані. Обробники відправки (submit handlers) надсилають старі значення. Компонент здається робочим, але форма фактично «сліпа». Якщо ваш кастомний контрол приймає введення користувача, але навколишня форма цього не помічає, причиною майже завжди є саме це.
registerOnTouched: Повідомлення про взаємодію
Форми відстежують не лише значення. Вони відстежують, чи взаємодіяв користувач із полем. Angular використовує стан touched, щоб вирішити, коли доречно показувати помилки валідації. Обов'язкове текстове поле не повинно спалахувати червоним у момент завантаження сторінки. Воно має чекати, поки користувач перейде до наступного поля або клікне в іншому місці.
Нативні інпути обробляють це автоматично через події blur. Кастомні компоненти — ні. Ви повинні використовувати registerOnTouched(fn), щоб самостійно повідомляти про ці взаємодії. Angular надає вам ще один callback; ви викликаєте його, коли вважаєте, що користувач суттєво взаємодіяв з елементом керування.
Точний момент залежить від вашого компонента. Для кастомного текстового інпуту ви можете викликати його під час blur. Для зірочкового рейтингу правильним моментом, ймовірно, буде перший клік. Для color picker, що відкриває popover, ви можете зачекати, поки палітра закриється. Головне — послідовність. Якщо ви ніколи не викликаєте callback для touched, Angular продовжуватиме позначати контрол як pristine. Помилки валідації залишатимуться прихованими навіть після того, як користувач явно закінчив редагування. Це призводить до плутанини та поганого користувацького досвіду.
setDisabledState: Дотримання команд форми
Динамічні форми постійно вмикають та вимикають поля на основі бізнес-логіки. Коли ви викликаєте .disable() на FormControl, Angular потребує, щоб ваш кастомний компонент відреагував. setDisabledState(isDisabled) приймає булеве значення. Коли воно true, ви повинні заблокувати свій UI.
Це означає більше, ніж просто ігнорування кліків. Ви повинні вимкнути внутрішні кнопки, прибрати стани фокусування та застосувати візуальні ефекти, такі як знижена прозорість або pointer-events: none. Якщо ви ігноруєте цей метод, ваш компонент залишається повністю інтерактивним, тоді як модель форми стверджує, що він вимкнений. Це створює трудновідстежувані баги. Користувачі можуть змінювати значення, які форма нібито відхиляє. Кнопки збереження можуть ставати активними на основі невалідних станів. Група форми та UI починають розходитися.
Добре побудований кастомний контрол розглядає setDisabledState як першочергову вимогу, а не як щось, про що згадали потім.
Помилки, які коштуватимуть вам часу на відлагодження
Кілька повторюваних помилок часто припускаються розробники, які тільки знайомляться з цим інтерфейсом.
Забуваєте викликати callback зміни. Ваш компонент оновлює свій внутрішній стан, але форма про це ніколи не дізнається. Валідатори зупиняються, а батьківські форми надсилають застарілі дані. Завжди викликайте збережену функцію onChange у той момент, коли користувач підтверджує нове значення.
Пропускаєте callback touched. Без нього Angular ніколи не позначить контрол як touched. Повідомлення про помилки, прив'язані до станів touched або dirty, не відображатимуться. Користувачі дивляться на форму, яка виглядає правильно, але не відправляється, і не бачать жодних ознак того, що саме не так.
Ігнорування стану disabled. Візуально активний контрол, який форма вважає вимкненим, порушує межу довіри. Користувач може продовжувати друкувати або клікати, але модель ігнорує його дії. Або, що ще гірше, модель може спорадично перезаписувати його введення під час циклів синхронізації.
Пропуск провайдера NG_VALUE_ACCESSOR. Це «тихий убивця». Якщо ви реалізуєте чотири методи, але забудете додати NG_VALUE_ACCESSOR до масиву providers вашого компонента, Angular ніколи не зареєструє ваш компонент як value accessor. Код компілюється. Представлення рендериться. Нічого не прив'язується. Помилок немає — просто компонент повністю існує окремо від форми. Завжди додавайте його в метадані декоратора.
Signals, валідатори та сучасний Angular
ControlValueAccessor — це не застарілий API. Він органічно вписується в сучасну розробку на Angular. Незалежно від того, чи керуєте ви внутрішнім станом за допомогою Signals, звичайних властивостей чи RxJS subjects, ці чотири методи залишаються вашим публічним контрактом із модулем форм. Ви споживаєте значення у writeValue, змінюєте свої Signals або стан і передаєте дані через callback-и, які надає Angular.
Стандартні валідатори працюють без змін. Validators.required, Validators.min, Validators.pattern та кастомні крос-польові валідатори оцінюють ваш компонент на базі CVA точно так само, як і нативний input. Form control бачить значення та стан. Йому байдуже, чи прийшло це значення з текстового поля, чи з власноруч створеного вибору місяця.
Саме завдяки цій портативності CVA є важливим для дизайн-систем та спільних UI-бібліотек. Одна команда створює надійне поле для введення номера телефону або віджет завантаження файлів. Вони реалізують інтерфейс один раз. Кожна інша команда в організації просто вставляє його у свої Reactive Forms без жодного додаткового налаштування. Компонент поводиться передбачувано, валідується однаково та передбачувано вимикається в кожному функціональному модулі.
Головний висновок
ControlValueAccessor — це не просто черговий інтерфейс, який потрібно вивчити для співбесіди. Це міст, який дозволяє вашим кастомним компонентам бути повноцінними учасниками екосистеми форм Angular, нарівні з нативними HTML-елементами. Опанувати його означає розуміти повний діалог між вашим віджетом і формою: отримання значень, повідомлення про зміни, сповіщення про дотики (touches) та дотримання станів вимкнення. Налаштуйте ці чотири елементи правильно, і ви зможете створювати складні, багаторазові контроли форм, які будуть непомітними для розробників, що ними користуються. Це і є ознака професійного Angular-компонента.
