Angular provides many built-in form controls such as <input>, <textarea>, <select>, and checkboxes. These elements work seamlessly with both Template-driven Forms and Reactive Forms because Angular already knows how to communicate with them.
But what happens when you build your own reusable component?
Imagine creating:
- A star rating component ⭐
- A color picker 🎨
- A date range selector 📅
- A phone number input 📞
- A rich text editor ✍️
By default, Angular has no idea how these components should exchange values with a form.
This is exactly why ControlValueAccessor (CVA) exists.
What is ControlValueAccessor?
ControlValueAccessor is an Angular interface that acts as a bridge between:
- Angular Forms API
- Your custom component
It teaches Angular how to:
- Write values into your component
- Read values from your component
- Mark the component as touched
- Enable or disable the component
Without CVA, Angular treats your component as just another HTML element.
With CVA, Angular treats it exactly like a native <input>.
Think of CVA Like a Translator
Imagine two people speaking different languages.
- Angular Forms speaks one language.
- Your custom component speaks another.
ControlValueAccessor translates between them.
Reactive Form
|
|
ControlValueAccessor
|
|
Custom Component
Why Do We Need It?
Suppose you build this component:
<app-counter></app-counter>
<form [formGroup]="form">
<app-counter formControlName="quantity"></app-counter>
</form>
Angular immediately throws an error:
No value accessor for form control with name: 'quantity'
Why?
Because Angular doesn't know:
- How to pass the value
- How to receive updates
- When the control becomes touched
- How to disable it
CVA solves all of this.
The ControlValueAccessor Interface
The interface looks like this:
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
Only four methods.
But each has an important responsibility.
Method 1 — writeValue()
Angular calls this whenever the form value changes.
Example:
this.form.patchValue({
quantity: 10
});
Angular internally executes:
component.writeValue(10);
Your implementation:
writeValue(value: number): void {
this.value = value;
}
Think of it as:
Form ➜ Component
Method 2 — registerOnChange()
Angular gives you a callback function.
Whenever your component value changes, you must call it.
registerOnChange(fn: any): void {
this.onChange = fn;
}
Later:
increment() {
this.value++;
this.onChange(this.value);
}
Flow:
User Clicks
↓
Component Updates
↓
onChange(value)
↓
Angular Form Updates
Think of it as:
Component ➜ Form
Method 3 — registerOnTouched()
Angular also provides another callback.
This tells Angular that the user interacted with the control.
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
When should you call it?
Usually on:
- blur
- click
- focus lost
- first interaction
Example:
onBlur() {
this.onTouched();
}
Now Angular knows:
control.touched === true
Method 4 — setDisabledState()
When someone writes:
control.disable();
Angular calls:
setDisabledState(true);
Implementation
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
Then your template:
<button [disabled]="disabled">
Now disabling works exactly like native inputs.
The Data Flow
Form.setValue()
|
|
writeValue()
|
|
Custom Component
|
User Interaction
|
|
onChange()
|
|
Reactive Form
A Complete Counter Component
counter.component.ts
import {
Component,
forwardRef
} from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR
} from '@angular/forms';
@Component({
selector: 'app-counter',
templateUrl: './counter.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CounterComponent),
multi: true
}
]
})
export class CounterComponent implements ControlValueAccessor {
value = 0;
disabled = false;
onChange = (_: number) => {};
onTouched = () => {};
writeValue(value: number): void {
this.value = value ?? 0;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
increment() {
if (this.disabled) return;
this.value++;
this.onChange(this.value);
this.onTouched();
}
decrement() {
if (this.disabled) return;
this.value--;
this.onChange(this.value);
this.onTouched();
}
}
counter.component.html
<button (click)="decrement()" [disabled]="disabled">
-
</button>
<span>{{ value }}</span>
<button (click)="increment()" [disabled]="disabled">
+
</button>
Parent Component
form = this.fb.group({
quantity: [5]
});
<form [formGroup]="form">
<app-counter formControlName="quantity"></app-counter>
</form>
Everything now works exactly like:
<input formControlName="quantity">
Understanding the Lifecycle
When the page loads:
Angular Creates Component
↓
registerOnChange()
↓
registerOnTouched()
↓
writeValue(initialValue)
When the user clicks "+" :
value++
↓
onChange(value)
↓
FormControl Updated
When the form resets:
form.reset()
↓
writeValue(null)
↓
Component Updated
When disabled:
control.disable()
↓
setDisabledState(true)
Common Mistakes
Mistake #1
Changing the value without calling onChange
Wrong:
this.value++;
Correct:
this.value++;
this.onChange(this.value);
Mistake #2
Never calling onTouched
Wrong:
click() {
this.value++;
}
Correct:
click() {
this.value++;
this.onTouched();
}
Mistake #3
Ignoring disabled state
Wrong
increment() {
this.value++;
}
Correct:
if (this.disabled) return;
Mistake #4
Forgetting NG_VALUE_ACCESSOR
Without:
providers: [
...
]
Angular cannot discover your accessor.
CVA with Validators
CVA only handles value communication.
Validation is separate.
You can still use:
Validators.required
Validators.min(1)
Validators.max(10)
Example:
quantity: new FormControl(
1,
Validators.min(1)
)
Angular validates normally.
CVA + Signals (Angular 17+)
Instead of:
value = 0;
You can use:
value = signal(0);
Writing value:
writeValue(value: number) {
this.value.set(value);
}
Updating:
increment() {
this.value.update(v => v + 1);
this.onChange(this.value());
}
Signals work perfectly with CVA and make state updates more declarative.
Real-World Components That Use CVA
Almost every reusable form component benefits from implementing ControlValueAccessor:
- Custom dropdowns
- Searchable selects
- Multi-select components
- Tag inputs
- Date pickers
- Time pickers
- Color pickers
- File uploaders
- Phone number inputs
- OTP inputs
- Rich text editors
- Sliders
- Range selectors
- Rating components
- Toggle switches
- Tree selectors
- Custom checkboxes
- Custom radio buttons
- Markdown editors
Best Practices
- Keep
writeValue()free of side effects; update internal state only. - Always call
onChange()when the value changes due to user interaction, not whenwriteValue()is invoked by Angular. - Call
onTouched()when the user has meaningfully interacted with the control (often on blur, sometimes on the first interaction depending on the UX). - Respect the disabled state in both your component logic and template.
- Expose a single source of truth for the value to avoid synchronization bugs.
- Combine CVA with
OnPushchange detection or Signals for better performance in reusable components. - Make your component accessible by supporting keyboard navigation, focus management, and ARIA attributes.
Final Thoughts
ControlValueAccessor is one of the most important APIs for building professional Angular applications. It allows your custom components to integrate seamlessly with Angular Forms, making them behave exactly like native form controls. By correctly implementing writeValue(), registerOnChange(), registerOnTouched(), and setDisabledState(), you unlock support for Reactive Forms, Template-driven Forms, validation, form resets, disabling, and all the other powerful features of Angular's forms ecosystem.
Mastering CVA is essential if you want to create reusable UI libraries, design systems, or enterprise-grade Angular applications where custom form controls are a common requirement. Once you understand the data flow between Angular Forms and your component, implementing new form controls becomes a predictable and repeatable process.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.