Angular forms świetnie współpracują ze standardowym HTML-em. Elementy input, textarea i select bez dodatkowego wysiłku pasują do Reactive Forms. Framework rozumie ich zdarzenia, wartości oraz stany.

Jednak nowoczesne aplikacje rzadko opierają się wyłącznie na standardowych elementach. Możesz potrzebować widgetu oceny w gwiazdkach, złożonego selektora daty lub niestandardowego wyboru koloru. Jeśli wrzucisz któryś z nich do grupy formularza (form group), Angular potraktuje go jako „martwy” HTML. patchValue nie zrobi nic. Walidatory go zignorują. Formularz nie będzie miał pojęcia, kiedy użytkownik wejdzie w interakcję z kontrolką, a form.disable() pozostawi niestandardowy widget w pełni interaktywnym.

To jest problem, który ma rozwiązać ControlValueAccessor.

Co tak naprawdę robi ControlValueAccessor

ControlValueAccessor to kontrakt, który zmienia niestandardowy komponent w pełnoprawnego obywatela formularza. Działa on jako tłumacz między Angular Forms API a Twoim własnym interfejsem użytkownika (UI). Gdy poprawnie go zaimplementujesz, Twój komponent stanie się nieodróżnialny od natywnego pola wejściowego z punktu widzenia formularza. Może on otrzymywać wartości, emitować zmiany, raportować interakcje (touches) i respektować stany wyłączone (disabled), dokładnie tak jak wbudowany element.

Interfejs wymaga czterech konkretnych metod. Każda z nich obsługuje inny kierunek komunikacji.

writeValue: Z formularza do komponentu

writeValue(obj) to pas ruchu przychodzącego. Za każdym razem, gdy model formularza zostaje zaktualizowany i musi przesłać nową wartość do Twojego UI, Angular wywołuje tę metodę. Jeśli wywołasz patchValue({ rating: 4 }) na grupie formularza, wartość 4 dotrze do Twojego komponentu poprzez writeValue. Jeśli zresetujesz formularz, writeValue otrzyma nową wartość początkową lub null. Twoim zadaniem w tej metodzie jest pobranie tych przychodzących danych i zmapowanie ich na wewnętrzny stan komponentu. Jeśli budujesz wybór koloru, writeValue otrzyma ciąg hex, np. #ff4400, a Ty musisz zaktualizować widok, aby pokazać ten kolor jako wybrany.

Pojawia się tutaj pewna praktyczna trudność. Angular może wywołać writeValue przed pełną inicjalizacją widoku, szczególnie w przypadku komponentów renderowanych dynamicznie, dialogów lub interfejsów opartych na kartach. Jeśli Twój komponent spróbuje odwołać się do DOM lub komponentów potomnych zbyt wcześnie, możesz napotkać błędy w czasie wykonywania (runtime errors). Dobrym wzorcem jest zapisanie wartości w lokalnej właściwości i zastosowanie jej po zainicjowaniu widoku lub zabezpieczenie się przed nieokreślonymi (undefined) referencjami do dzieci. Nigdy nie zakładaj, że writeValue wywołuje się tylko wtedy, gdy szablon jest stabilny.

registerOnChange: Z komponentu do formularza

registerOnChange(fn) konfiguruje pas ruchu wychodzącego. Angular przekazuje Ci funkcję zwrotną (callback), do której musisz zachować referencję. Za każdym razem, gdy użytkownik zmieni wartość wewnątrz Twojego komponentu, wywołujesz tę funkcję z nową wartością. W komponencie oceny w gwiazdkach, gdy użytkownik kliknie trzecią gwiazdkę, wywołujesz zapisaną funkcję zwrotną z wartością 3. To wywołanie wraca do FormControl, aktualizuje model, wyzwala wszelkie subskrypcje valueChanges i ponownie uruchamia walidatory.

Pominięcie tego kroku to najczęstszy sposób na „ciche” zepsucie formularza. Widget może wyglądać na działający. Użytkownik widzi zapalające się gwiazdki, zmieniające się kolory lub wypełniające się daty. Jednak model formularza nigdy się nie aktualizuje. Walidatory nadal oceniają nieaktualne dane. Obsługiwalniki wysyłania (submit handlers) przesyłają stare wartości. Komponent wydaje się działać, ale formularz jest w rzeczywistości „ślepy”. Jeśli Twoja niestandardowa kontrolka przyjmuje dane od użytkownika, ale otaczający ją formularz nigdy tego nie zauważa, prawie zawsze jest to właśnie przyczyna.

registerOnTouched: Raportowanie interakcji

Formularze nie śledzą tylko wartości. Śledzą również, czy użytkownik wszedł w interakcję z polem. Angular wykorzystuje stan touched, aby zdecydować, kiedy odpowiednie jest wyświetlenie błędów walidacji. Wymagane pole tekstowe nie powinno błyskawicznie świecić na czerwono w momencie ładowania strony. Powinno poczekać, aż użytkownik przejdzie do następnego pola (tab) lub kliknie gdzie indziej.

Natywne pola wejściowe obsługują to automatycznie poprzez zdarzenia blur. Niestandardowe komponenty – nie. Musisz użyć registerOnTouched(fn), aby samodzielnie raportować te interakcje. Angular przekazuje Ci kolejną funkcję zwrotną; wywołujesz ją, gdy uznasz, że użytkownik wszedł w istotną interakcję z kontrolką.

Dokładny moment zależy od Twojego komponentu. W przypadku niestandardowego pola tekstowego możesz wywołać ją przy zdarzeniu blur. W przypadku oceny w gwiazdkach prawdopodobnie właściwym momentem będzie pierwsze kliknięcie. W przypadku wyboru koloru, który otwiera okienko (popover), możesz poczekać, aż paleta zostanie zamknięta. Kluczem jest spójność. Jeśli nigdy nie wywołasz funkcji zwrotnej touched, Angular będzie nadal oznaczał kontrolkę jako pristine. Błędy walidacji pozostaną ukryte, nawet gdy użytkownik wyraźnie zakończył edycję. Prowadzi to do dezorientacji i złego doświadczenia użytkownika (UX).

setDisabledState: Respecting Form Commands

Dynamic forms constantly enable and disable fields based on business logic. When you call .disable() on a FormControl, Angular needs your custom component to respond. setDisabledState(isDisabled) receives a boolean. When it is true, you should lock down your UI.

This means more than just ignoring clicks. You should disable internal buttons, remove focusable states, and apply visual treatments like reduced opacity or pointer-events: none. If you ignore this method, your component stays fully interactive while the form model insists it is disabled. That creates hard-to-trace bugs. Users can modify values that the form supposedly rejects. Save buttons might enable based on invalid states. The form group and the UI drift apart.

A well-built custom control treats setDisabledState as a first-class requirement, not an afterthought.

Mistakes That Will Cost You Debugging Time

Several recurring mistakes trip up developers who are new to this interface.

Forgetting to call the change callback. Your component updates its internal state, but the form never hears about it. Validators stall, and parent forms submit stale data. Always fire that stored onChange function the moment the user commits a new value.

Skipping the touched callback. Without it, Angular never marks the control as touched. Error messages tied to touched or dirty states refuse to show. Users stare at a form that looks correct but will not submit, with no visible indication of what is wrong.

Neglecting the disabled state. A visually enabled control that the form thinks is disabled creates a broken trust boundary. The user can keep typing or clicking, but the model ignores them. Or worse, the model sporadically overwrites their input during sync cycles.

Omitting the NG_VALUE_ACCESSOR provider. This is the silent killer. If you implement the four methods but forget to add the NG_VALUE_ACCESSOR to your component’s providers array, Angular never registers your component as a value accessor. The code compiles. The view renders. Nothing binds. There is no error message, just a component that floats outside the form entirely. Always include it in the decorator metadata.

Signals, Validators, and Modern Angular

ControlValueAccessor is not legacy API surface. It fits cleanly into modern Angular development. Whether you manage internal state with Signals, plain properties, or RxJS subjects, the four methods remain your public contract with the forms module. You consume values in writeValue, mutate your Signals or state, and emit through the callbacks Angular provides.

Standard validators work without modification. Validators.required, Validators.min, Validators.pattern, and custom cross-field validators all evaluate your CVA-backed component exactly as they would a native input. The form control sees a value and a state. It does not care whether that value came from a text box or a hand-crafted month-picker.

That portability is why CVA matters for design systems and shared UI libraries. One team builds a robust phone-number input or a file upload widget. They implement the interface once. Every other team in the organization drops it into their Reactive Forms with zero additional wiring. The component behaves predictably, validates uniformly, and disables consistently across every feature module.

The Real Takeaway

ControlValueAccessor is not just another interface to memorize for interview questions. It is the bridge that lets your custom components participate in Angular’s form ecosystem as equals to native HTML elements. Mastering it means understanding the full conversation between your widget and the form: receiving values, reporting changes, announcing touches, and respecting disabled states. Get these four pieces right, and you can build complex, reusable form controls that feel invisible to the developers who use them. That is the mark of a professional Angular component.