Goodbye ControlValueAccessor, Hello Signals

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-Based FormValueControl 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.

Here is the "Before" (CVA) vs. "After" (Signals):

🚫 The Old Way (ControlValueAccessor)

(Trigger warning: Boilerplate)

@Component({ ... })
export class OldInputComponent implements ControlValueAccessor {
  value: string = '';
  onChange = (val: string) => {};
  onTouched = () => {};

  writeValue(value: string): void {
    this.value = value;
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  // ... and don't forget the providers array!
}

✨ The New Way (Signal Forms)

(Look at how clean this is!)

@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.

class MyFancyInput implements FormValueControl {
  readonly value = model('');
  readonly disabled = input(false);
  readonly touched = input(false);
  readonly dirty = input(false);
  readonly valid = input(true);
  readonly invalid = input(false);
  readonly pending = input(false);
  readonly required = input(false);
  readonly errors = input([]);
}

The Future is Reactive

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! 🎉