I form di Angular funzionano magnificamente con l'HTML standard. input, textarea e select si integrano perfettamente nei Reactive Forms senza sforzi aggiuntivi. Il framework ne comprende gli eventi, i valori e gli stati.
Ma le applicazioni moderne raramente si accontentano dei soli elementi standard. Potresti aver bisogno di un widget per la valutazione a stelle, di un selettore di date composito o di un color picker personalizzato. Se inserisci uno di questi in un form group, Angular lo tratterà come HTML morto. patchValue non farà nulla. I validator lo ignoreranno. Il form non avrà idea di quando un utente interagisce con il controllo, e form.disable() lascerà il widget personalizzato completamente interattivo.
Questo è il problema che ControlValueAccessor esiste per risolvere.
Cosa fa effettivamente ControlValueAccessor
ControlValueAccessor è il contratto che trasforma un componente personalizzato in un cittadino di prima classe dei form. Agisce come un traduttore tra l'Angular Forms API e la tua UI. Una volta implementato correttamente, il tuo componente diventa indistinguibile da un input nativo dal punto di vista del form. Può ricevere valori, emettere cambiamenti, segnalare i "touch" e rispettare gli stati disabilitati proprio come un elemento integrato.
L'interfaccia richiede quattro metodi specifici. Ognuno gestisce una distinta direzione di comunicazione.
writeValue: Dal Form al Componente
writeValue(obj) è la corsia in entrata. Ogni volta che il modello del form si aggiorna e deve spingere un nuovo valore nella tua UI, Angular chiama questo metodo. Se invochi patchValue({ rating: 4 }) su un form group, quel valore 4 arriva all'interno del tuo componente tramite writeValue. Se resetti il form, writeValue riceve il nuovo valore iniziale o null. Il tuo compito all'interno di questo metodo è prendere i dati in entrata e mapparli sullo stato interno del componente. Se stai costruendo un color picker, writeValue riceve una stringa esadecimale come #ff4400 e devi aggiornare la tua view per mostrare quel colore come selezionato.
C'è un intoppo pratico qui. Angular può chiamare writeValue prima che la tua view sia completamente inizializzata, specialmente all'interno di componenti renderizzati dinamicamente, dialog o interfacce a schede (tab). Se il tuo componente prova a toccare il DOM o i componenti figli troppo presto, potresti incorrere in errori a runtime. Un pattern solido consiste nello memorizzare il valore in una proprietà locale e applicarlo dopo l'inizializzazione della view, oppure nel proteggersi da riferimenti ai figli non definiti. Non dare mai per scontato che writeValue venga eseguito solo quando il template è stabile.
registerOnChange: Dal Componente al Form
registerOnChange(fn) configura la corsia in uscita. Angular ti fornisce una funzione di callback e tu devi conservarne un riferimento. Ogni volta che l'utente cambia il valore all'interno del tuo componente, chiami quella funzione con il nuovo valore. In un componente di valutazione a stelle, quando l'utente clicca sulla terza stella, invochi la callback memorizzata con 3. Quella chiamata torna indietro al FormControl, aggiorna il modello, attiva eventuali sottoscrizioni a valueChanges ed esegue nuovamente i validator.
Saltare questo passaggio è il modo più comune per rompere silenziosamente un form. Il widget potrebbe sembrare attivo. L'utente vede le stelle illuminarsi, i colori cambiare o le date popolarsi. Ma il modello del form non si aggiorna mai. I validator continuano a valutare dati obsoleti. Gli handler di invio (submit) inviano valori vecchi. Il componente sembra funzionare, eppure il form è di fatto cieco. Se il tuo controllo personalizzato accetta l'input dell'utente ma il form circostante non se ne accorge mai, il colpevole è quasi sempre questo.
registerOnTouched: Segnalare l'interazione
I form non tracciano solo i valori. Tracciano se un utente ha interagito con un campo. Angular utilizza lo stato touched per decidere quando è appropriato mostrare gli errori di validazione. Un input di testo obbligatorio non dovrebbe diventare rosso nell'istante in cui la pagina viene caricata. Dovrebbe aspettare finché l'utente non passa al campo successivo (tab) o clicca altrove.
Gli input nativi gestiscono questo aspetto automaticamente tramite gli eventi di blur. I componenti personalizzati no. Devi usare registerOnTouched(fn) per segnalare tu stesso queste interazioni. Angular ti fornisce un'altra callback; la chiami quando decidi che l'utente ha interagito in modo significativo con il controllo.
La tempistica esatta dipende dal tuo componente. Per un input personalizzato simile a un testo, potresti chiamarla al blur. Per una valutazione a stelle, il primo clic è probabilmente il momento giusto. Per un color picker che apre un popover, potresti aspettare che la tavolozza si chiuda. La chiave è la coerenza. Se non chiami mai la callback touched, Angular continuerà a contrassegnare il controllo come pristine. Gli errori di validazione rimarranno nascosti anche dopo che l'utente ha chiaramente finito di modificare. Ciò porta a confusione e a una scarsa esperienza utente.
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.
