Angular forms fonctionnent parfaitement avec le HTML standard. Les éléments input, textarea et select s'intègrent dans les Reactive Forms sans effort supplémentaire. Le framework comprend leurs événements, leurs valeurs et leurs états.
Mais les applications modernes se contentent rarement des seuls éléments standards. Vous pourriez avoir besoin d'un widget de notation par étoiles, d'un sélecteur de date composite ou d'un sélecteur de couleur personnalisé. Si vous insérez l'un de ces éléments dans un groupe de formulaire, Angular le traitera comme du HTML mort. patchValue ne fera rien. Les validateurs l'ignoreront. Le formulaire n'aura aucune idée du moment où l'utilisateur interagit avec le contrôle, et form.disable() laissera le widget personnalisé entièrement interactif.
C'est précisément le problème que ControlValueAccessor est conçu pour résoudre.
Ce que fait réellement ControlValueAccessor
ControlValueAccessor est le contrat qui transforme un composant personnalisé en un citoyen de premier rang des formulaires. Il agit comme un traducteur entre l'API Angular Forms et votre propre interface utilisateur. Une fois correctement implémenté, votre composant devient indiscernable d'un input natif du point de vue du formulaire. Il peut recevoir des valeurs, émettre des changements, signaler les interactions (touches) et respecter les états désactivés, tout comme un élément intégré.
L'interface nécessite quatre méthodes spécifiques. Chacune gère une direction de communication distincte.
writeValue : Du formulaire vers le composant
writeValue(obj) est la voie de réception. Chaque fois que le modèle du formulaire est mis à jour et doit pousser une nouvelle valeur dans votre interface utilisateur, Angular appelle cette méthode. Si vous invoquez patchValue({ rating: 4 }) sur un groupe de formulaire, la valeur 4 arrive dans votre composant via writeValue. Si vous réinitialisez le formulaire, writeValue reçoit la nouvelle valeur initiale ou null. Votre rôle dans cette méthode est de prendre ces données entrantes et de les mapper sur l'état interne de votre composant. Si vous construisez un sélecteur de couleur, writeValue reçoit une chaîne hexadécimale comme #ff4400, et vous devez mettre à jour votre vue pour afficher cette couleur comme étant sélectionnée.
Il y a ici une subtilité pratique. Angular peut appeler writeValue avant que votre vue ne soit complètement initialisée, en particulier à l'intérieur de composants rendus dynamiquement, de boîtes de dialogue ou d'interfaces à onglets. Si votre composant tente d'accéder au DOM ou aux composants enfants trop tôt, vous risquez de rencontrer des erreurs d'exécution. Un modèle solide consiste à stocker la valeur dans une propriété locale et à l'appliquer après l'initialisation de la vue, ou à se protéger contre les références d'enfants indéfinies. Ne partez jamais du principe que writeValue ne se déclenche que lorsque votre template est stable.
registerOnChange : Du composant vers le formulaire
registerOnChange(fn) établit la voie de sortie. Angular vous remet une fonction de rappel (callback), et vous devez en conserver une référence. Chaque fois que l'utilisateur modifie la valeur à l'intérieur de votre composant, vous appelez cette fonction avec la nouvelle valeur. Dans un composant de notation par étoiles, lorsque l'utilisateur clique sur la troisième étoile, vous invoquez le callback stocké avec la valeur 3. Cet appel remonte vers le FormControl, met à jour le modèle, déclenche toutes les souscriptions à valueChanges et réexécute les validateurs.
Sauter cette étape est le moyen le plus courant de casser silencieusement un formulaire. Le widget peut sembler fonctionnel. L'utilisateur voit les étoiles s'allumer, les couleurs changer ou les dates se remplir. Mais le modèle du formulaire ne se met jamais à jour. Les validateurs continuent d'évaluer des données obsolètes. Les gestionnaires de soumission (submit handlers) envoient d'anciennes valeurs. Le composant semble fonctionner, pourtant le formulaire est de fait aveugle. Si votre contrôle personnalisé accepte la saisie de l'utilisateur mais que le formulaire environnant ne le remarque jamais, c'est presque toujours le coupable.
registerOnTouched : Signaler l'interaction
Les formulaires ne se contentent pas de suivre les valeurs. Ils suivent si un utilisateur a interagi avec un champ. Angular utilise l'état "touched" pour décider s'il est approprié d'afficher des erreurs de validation. Un champ de texte obligatoire ne devrait pas clignoter en rouge dès le chargement de la page. Il devrait attendre que l'utilisateur change de champ (via la touche Tab) ou clique ailleurs.
Les éléments natifs gèrent cela automatiquement via les événements blur. Les composants personnalisés non. Vous devez utiliser registerOnTouched(fn) pour signaler vous-même ces interactions. Angular vous donne un autre callback ; vous l'appelez lorsque vous estimez que l'utilisateur a interagi de manière significative avec le contrôle.
Le moment exact dépend de votre composant. Pour un champ de saisie de texte personnalisé, vous pourriez l'appeler lors de l'événement blur. Pour une notation par étoiles, le premier clic est probablement le bon moment. Pour un sélecteur de couleur qui ouvre une fenêtre contextuelle (popover), vous pourriez attendre la fermeture de la palette. La clé est la cohérence. Si vous n'appelez jamais le callback "touched", Angular continuera de marquer le contrôle comme pristine. Les erreurs de validation resteront cachées même après que l'utilisateur a clairement fini sa modification. Cela entraîne de la confusion et une mauvaise expérience utilisateur.
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.
