If you’ve been building custom form controls in Angular for a while, you know what to expect. You want to wrap a simple <input> or create a fancy custom slider, and suddenly you’re implementing ControlValueAccessor (CVA).
You have to implement writeValue, registerOnChange, registerOnTouched, and setDisabledState. You have to manage internal state, fire callbacks at the right time, and handle the boilerplate. It’s a rite of passage, but let's be honest: it’s a lot of code for something that should be simple.
Currently a custom input component with CVA looks something like this:
import { Component, forwardRef, input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-custom-input',
template: `
@if (label()) {
}
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
export class CustomInputComponent implements ControlValueAccessor {
readonly label = input('');
readonly type = input('text');
// Internal state
value: string = '';
isDisabled: boolean = false;
// Placeholder functions for Angular's callbacks
onChange = (value: string) => {};
onTouched = () => {};
// --- ControlValueAccessor Implementation ---
// 1. Called by Angular to write a value to the component (Model -> View)
writeValue(value: string): void {
this.value = value || '';
}
// 2. Registers the callback Angular provides to listen for changes (View -> Model)
registerOnChange(fn: any): void {
this.onChange = fn;
}
// 3. Registers the callback Angular provides to know when the control was interacted with
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
// 4. (Optional) Called by Angular when the form control is disabled/enabled
setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
// --- UI Event Handlers ---
onInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.value = target.value;
// Notify Angular that the value changed
this.onChange(this.value);
}
onBlur(): void {
// Notify Angular that the input lost focus (marks the control as 'touched')
this.onTouched();
}
}
That is about to change.
With Pull Request #67267, Angular is bridging the gap between the new Signal-Forms architecture and the Template/Reactive Forms. This isn't just a minor update—it’s a fundamental shift in how we author custom form controls.
The New Way: FormValueControl
Imagine creating a custom input where the "form logic" is just… a Signal.
Thanks to this new PR, Angular now supports Signal-BasedFormValueControl components via the FormValueControl interface (which means we can use the same component for all three Forms systems in Angular). The framework can now detect if your component exposes a value model signal and automatically sync it with ngModel or FormControl.
@Component({
selector: 'fancy-input',
template: `
`,
})
export class FancyInput implements FormValueControl {
// This is it. This is the API.
value = model('');
}
And just like that, you can use it immediately in your forms:
Why You Should Care?
1. Zero Boilerplate
You won't have to register callbacks and manually fire onChange events. The model() signal handles the two-way binding naturally. Angular observes the signal and updates the form control, and vice-versa.
2. Seamless Integration
This isn't a "new forms module" that requires you to rewrite your entire app. This PR adds support for these signal-based controls inside existing Template and Reactive Forms. You can start building new controls this way today and drop them right into your existing [formGroup].
3. Automatic Status Sync
It’s not just about the value. This update ensures that validation status, "touched" state, and "dirty" state are all synchronized.
This change is a massive win for Developer Experience (DX). It lowers the barrier to entry for creating reusable form components and fully embraces Angular's Signal era.
Get ready to delete a lot of registerOnChange code! 🎉
Intermediate workshop
Modern Design Systems with Angular
Learn how to effectively use variables and design tokens in Figma, and apply them to style components. Master the creation of components in Storybook to build cohesive design systems.