Angular forms work beautifully with standard HTML. input, textarea, and select all slot into Reactive Forms without extra effort. The framework understands their events, their values, and their states.

But modern applications rarely get by on standard elements alone. You might need a star rating widget, a composite date selector, or a custom color picker. Drop one of these into a form group, and Angular treats it as dead HTML. patchValue does nothing. Validators ignore it. The form has no idea when a user interacts with the control, and form.disable() leaves the custom widget fully interactive.

This is the problem ControlValueAccessor exists to solve.

What ControlValueAccessor Actually Does

ControlValueAccessor is the contract that turns a custom component into a first-class form citizen. It acts as a translator between the Angular Forms API and your own UI. Once you implement it correctly, your component becomes indistinguishable from a native input from the form’s point of view. It can receive values, emit changes, report touches, and respect disabled states just like a built-in element.

The interface requires four specific methods. Each one handles a distinct direction of communication.

writeValue: Form to Component

writeValue(obj) is the inbound lane. Whenever the form model updates and needs to push a new value into your UI, Angular calls this method. If you invoke patchValue({ rating: 4 }) on a form group, that value of 4 arrives inside your component through writeValue. If you reset the form, writeValue receives the new initial value or null. Your job inside this method is to take that incoming data and map it onto your component’s internal state. If you are building a color picker, writeValue receives a hex string like #ff4400, and you must update your view to show that color as selected.

There is a practical wrinkle here. Angular can call writeValue before your view is fully initialized, especially inside dynamically rendered components, dialogs, or tabbed interfaces. If your component tries to touch the DOM or child components too early, you can hit runtime errors. A solid pattern is to store the value in a local property and apply it after the view initializes, or to guard against undefined child references. Never assume writeValue only fires when your template is stable.

registerOnChange: Component to Form

registerOnChange(fn) sets up the outbound lane. Angular hands you a callback function, and you must keep a reference to it. Every time the user changes the value inside your component, you call that function with the new value. In a star rating component, when the user clicks the third star, you invoke the stored callback with 3. That call flows back into the FormControl, updates the model, triggers any valueChanges subscriptions, and re-runs validators.

Skipping this step is the most common way to silently break a form. The widget might look alive. The user sees stars light up, colors shift, or dates populate. But the form model never updates. Validators continue to evaluate stale data. Submit handlers send old values. The component appears to work, yet the form is effectively blind. If your custom control accepts user input but the surrounding form never notices, this is almost always the culprit.

registerOnTouched: Reporting Interaction

Forms do not just track values. They track whether a user has interacted with a field. Angular uses the touched state to decide when it is appropriate to show validation errors. A required text input should not flash red the instant the page loads. It should wait until the user tabs away or clicks elsewhere.

Native inputs handle this automatically through blur events. Custom components do not. You must use registerOnTouched(fn) to report these interactions yourself. Angular gives you another callback; you call it when you decide the user has meaningfully engaged with the control.

The exact timing depends on your component. For a text-like custom input, you might call it on blur. For a star rating, the first click is probably the right moment. For a color picker that opens a popover, you might wait until the palette closes. The key is consistency. If you never call the touched callback, Angular keeps marking the control as pristine. Validation errors stay hidden even after the user has clearly finished editing. That leads to confusion and poor user experience.

setDisabledState: Respetando los comandos del formulario

Los formularios dinámicos habilitan y deshabilitan campos constantemente basándose en la lógica de negocio. Cuando llamas a .disable() en un FormControl, Angular necesita que tu componente personalizado responda. setDisabledState(isDisabled) recibe un booleano. Cuando es true, deberías bloquear tu interfaz de usuario (UI).

Esto significa más que simplemente ignorar los clics. Deberías deshabilitar los botones internos, eliminar los estados enfocables y aplicar tratamientos visuales como una opacidad reducida o pointer-events: none. Si ignoras este método, tu componente permanecerá totalmente interactivo mientras el modelo del formulario insiste en que está deshabilitado. Eso crea errores difíciles de rastrear. Los usuarios pueden modificar valores que supuestamente el formulario rechaza. Los botones de guardado podrían habilitarse basándose en estados inválidos. El grupo del formulario y la UI se desincronizan.

Un control personalizado bien construido trata a setDisabledState como un requisito de primer orden, no como algo secundario.

Errores que te costarán tiempo de depuración

Varios errores recurrentes suelen confundir a los desarrolladores que son nuevos en esta interfaz.

Olvidar llamar al callback de cambio. Tu componente actualiza su estado interno, pero el formulario nunca se entera. Los validadores se detienen y los formularios padres envían datos obsoletos. Siempre ejecuta esa función onChange almacenada en el momento en que el usuario confirme un nuevo valor.

Omitir el callback de 'touched'. Sin él, Angular nunca marca el control como touched. Los mensajes de error vinculados a los estados touched o dirty no se muestran. Los usuarios se quedan mirando un formulario que parece correcto pero que no se envía, sin ninguna indicación visible de qué está mal.

Descuidar el estado deshabilitado. Un control visualmente habilitado que el formulario cree que está deshabilitado crea una ruptura en el límite de confianza. El usuario puede seguir escribiendo o haciendo clic, pero el modelo lo ignora. O peor aún, el modelo sobrescribe esporádicamente su entrada durante los ciclos de sincronización.

Omitir el proveedor NG_VALUE_ACCESSOR. Este es el asesino silencioso. Si implementas los cuatro métodos pero olvidas añadir el NG_VALUE_ACCESSOR al array de providers de tu componente, Angular nunca registrará tu componente como un value accessor. El código compila. La vista se renderiza. Nada se vincula. No hay mensaje de error, solo un componente que flota completamente fuera del formulario. Inclúyelo siempre en los metadatos del decorador.

Signals, Validators y Angular moderno

ControlValueAccessor no es una superficie de API heredada (legacy). Encaja perfectamente en el desarrollo moderno de Angular. Ya sea que gestiones el estado interno con Signals, propiedades simples o sujetos de RxJS, los cuatro métodos siguen siendo tu contrato público con el módulo de formularios. Consumes valores en writeValue, mutas tus Signals o tu estado, y emites a través de los callbacks que proporciona Angular.

Los validadores estándar funcionan sin modificaciones. Validators.required, Validators.min, Validators.pattern y los validadores personalizados entre campos evalúan tu componente basado en CVA exactamente igual que lo harían con un input nativo. El control del formulario ve un valor y un estado. No le importa si ese valor proviene de un cuadro de texto o de un selector de meses personalizado.

Esa portabilidad es la razón por la que CVA es importante para los sistemas de diseño y las librerías de UI compartidas. Un equipo construye un input de número de teléfono robusto o un widget de carga de archivos. Implementan la interfaz una vez. Todos los demás equipos de la organización lo integran en sus Reactive Forms sin necesidad de configuración adicional. El componente se comporta de manera predecible, valida uniformemente y se deshabilita de forma consistente en cada módulo de funcionalidad.

La conclusión real

ControlValueAccessor no es solo otra interfaz que memorizar para preguntas de entrevistas. Es el puente que permite que tus componentes personalizados participen en el ecosistema de formularios de Angular en igualdad de condiciones que los elementos HTML nativos. Dominarlo significa comprender la conversación completa entre tu widget y el formulario: recibir valores, reportar cambios, anunciar toques (touches) y respetar los estados deshabilitados. Si logras dominar estas cuatro piezas, podrás construir controles de formulario complejos y reutilizables que resulten invisibles para los desarrolladores que los utilizan. Esa es la marca de un componente de Angular profesional.