Os formulários do Angular funcionam perfeitamente com HTML padrão. input, textarea e select se encaixam nos Reactive Forms sem esforço adicional. O framework entende seus eventos, seus valores e seus estados.
Mas as aplicações modernas raramente se contentam apenas com elementos padrão. Você pode precisar de um widget de avaliação por estrelas, um seletor de data composto ou um seletor de cores personalizado. Ao inserir um desses em um form group, o Angular o trata como HTML morto. O patchValue não faz nada. Os validadores o ignoram. O formulário não tem ideia de quando um usuário interage com o controle, e o form.disable() deixa o widget personalizado totalmente interativo.
Este é o problema que o ControlValueAccessor existe para resolver.
O que o ControlValueAccessor realmente faz
O ControlValueAccessor é o contrato que transforma um componente personalizado em um cidadão de primeira classe dos formulários. Ele atua como um tradutor entre a Angular Forms API e sua própria UI. Uma vez implementado corretamente, seu componente torna-se indistinguível de um input nativo do ponto de vista do formulário. Ele pode receber valores, emitir mudanças, reportar toques (touches) e respeitar estados desabilitados, exatamente como um elemento integrado.
A interface requer quatro métodos específicos. Cada um lida com uma direção distinta de comunicação.
writeValue: Do Formulário para o Componente
writeValue(obj) é a via de entrada. Sempre que o modelo do formulário é atualizado e precisa enviar um novo valor para sua UI, o Angular chama este método. Se você invocar patchValue({ rating: 4 }) em um form group, esse valor de 4 chega ao seu componente através do writeValue. Se você resetar o formulário, o writeValue recebe o novo valor inicial ou null. Seu trabalho dentro deste método é pegar esses dados recebidos e mapeá-los para o estado interno do seu componente. Se você estiver construindo um seletor de cores, o writeValue recebe uma string hex como #ff4400, e você deve atualizar sua view para mostrar essa cor como selecionada.
Há um detalhe prático aqui. O Angular pode chamar o writeValue antes que sua view esteja totalmente inicializada, especialmente dentro de componentes renderizados dinamicamente, diálogos ou interfaces de abas. Se o seu componente tentar tocar o DOM ou componentes filhos cedo demais, você pode encontrar erros de tempo de execução (runtime errors). Um padrão sólido é armazenar o valor em uma propriedade local e aplicá-lo após a inicialização da view, ou proteger-se contra referências de filhos indefinidas. Nunca assuma que o writeValue só é disparado quando seu template está estável.
registerOnChange: Do Componente para o Formulário
registerOnChange(fn) configura a via de saída. O Angular entrega a você uma função de callback, e você deve manter uma referência a ela. Toda vez que o usuário alterar o valor dentro do seu componente, você chama essa função com o novo valor. Em um componente de avaliação por estrelas, quando o usuário clica na terceira estrela, você invoca o callback armazenado com 3. Essa chamada retorna para o FormControl, atualiza o modelo, aciona quaisquer inscrições de valueChanges e executa os validadores novamente.
Pular esta etapa é a maneira mais comum de quebrar um formulário silenciosamente. O widget pode parecer funcional. O usuário vê as estrelas acenderem, as cores mudarem ou as datas serem preenchidas. Mas o modelo do formulário nunca é atualizado. Os validadores continuam a avaliar dados obsoletos. Os manipuladores de envio (submit handlers) enviam valores antigos. O componente parece funcionar, mas o formulário está efetivamente "cego". Se o seu controle personalizado aceita a entrada do usuário, mas o formulário ao redor nunca percebe, este é quase sempre o culpado.
registerOnTouched: Reportando Interação
Formulários não rastreiam apenas valores. Eles rastreiam se um usuário interagiu com um campo. O Angular usa o estado touched para decidir quando é apropriado mostrar erros de validação. Um campo de texto obrigatório não deve ficar vermelho no instante em que a página carrega. Ele deve esperar até que o usuário saia do campo ou clique em outro lugar.
Inputs nativos lidam com isso automaticamente por meio de eventos de blur. Componentes personalizados não. Você deve usar registerOnTouched(fn) para reportar essas interações por conta própria. O Angular fornece outro callback; você o chama quando decide que o usuário interagiu de forma significativa com o controle.
O momento exato depende do seu componente. Para um input personalizado do tipo texto, você pode chamá-lo no blur. Para uma avaliação por estrelas, o primeiro clique é provavelmente o momento certo. Para um seletor de cores que abre um popover, você pode esperar até que a paleta seja fechada. A chave é a consistência. Se você nunca chamar o callback de touched, o Angular continuará marcando o controle como pristine. Os erros de validação permanecem ocultos mesmo depois que o usuário claramente terminou a edição. Isso leva à confusão e a uma experiência de usuário ruim.
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.
