| |

Angular 34 🅰️ Custom Form Controls

Native HTML form elements — <input>, <select>, <textarea> — are integrated into Angular’s forms system through built-in directives called value accessors. When you write formControlName="email" on an <input>, Angular’s DefaultValueAccessor handles the bridge between the DOM element and the FormControl instance. But the moment you need a control that is not an <input> — a star rating widget, a color picker, a composite address field, a counter with increment and decrement buttons — the native integration is gone. You are left with a component that has no idea how to talk to FormControl. The ControlValueAccessor interface is Angular’s answer to this problem. It is the contract that any component can implement to become a first-class form citizen, indistinguishable from a native input in the eyes of formControlName, ngModel, and the entire validation system. This chapter covers what ControlValueAccessor is, the four methods that make up the contract, how to register a component as a value accessor, how to combine it with validation, and the patterns that keep custom controls maintainable.

Key point: ControlValueAccessor is an interface with four methods: writeValue (model → view), registerOnChange (view → model), registerOnTouched (blur notification), and optionally setDisabledState (disabled handling) . A component implements these methods and registers itself with the NG_VALUE_ACCESSOR token using forwardRef to avoid circular dependency issues . Once registered, the component works with both reactive forms and template-driven forms — Angular does not distinguish . The component becomes a valid target for formControlName, formControl, and ngModel, and its validity participates in the parent form’s validity.


Why custom form controls exist

The set of native form controls is fixed and small. There is no <star-rating>, no <color-picker>, no <address-field> in the HTML specification. Angular’s built-in value accessors cover exactly the native elements: DefaultValueAccessor for text inputs and textareas, CheckboxControlValueAccessor for checkboxes, NumberValueAccessor for number inputs, RadioControlValueAccessor for radio buttons, RangeValueAccessor for range sliders, and SelectControlValueAccessor for selects . That is the complete list.

Why wrapping a native input is not enough. The naive approach is to build a component that contains an <input> inside its template, expose an @Input() for the value, and an @Output() for changes. Then the parent writes <my-input [value]="email" (valueChange)="email = $event">. This works in isolation but fails the moment the component is used inside a form. The parent cannot write formControlName="email" on <my-input> because formControlName only works with components that implement ControlValueAccessor. The component is invisible to the form model, its validity is not tracked, its touched state is not propagated, and the form cannot know whether it is valid .

Why the interface is the right abstraction. ControlValueAccessor is deliberately minimal: four methods that describe the entire contract between a form control and a view. writeValue is the model telling the view what to display. registerOnChange is the view giving the model a callback to call when the user changes something. registerOnTouched is the view telling the model when the user has interacted. setDisabledState is the model telling the view it is disabled. Every native element and every custom control satisfies this same contract, which is why Angular can treat them uniformly .

Why this matters for composition. A form built from custom controls behaves exactly like a form built from native inputs. Validation, dirty and touched tracking, setValue and patchValue, and form submission all work without special handling. The custom control is not a second-class citizen; it is indistinguishable from an <input> in the eyes of the form model .

Why the interface methods are not optional. writeValue, registerOnChange, and registerOnTouched are required. If a component claims to implement ControlValueAccessor but does not provide them, Angular will fail at runtime when it tries to call them. setDisabledState is optional, but omitting it means the control ignores disable() calls and remains interactive even when the form model says it should be disabled .


The four methods of ControlValueAccessor

The interface defines the complete contract. Understanding what each method does and when it is called is the foundation of every custom control.

interface ControlValueAccessor {
  writeValue(obj: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(isDisabled: boolean): void;
}

writeValue(value: any) is called by the forms API when the model changes and the view needs to update. It is the model-to-view direction. When the parent calls formControl.setValue("new value") or when the form is initialized with a value, Angular calls writeValue on the value accessor with the new value. The accessor is responsible for updating whatever internal state or DOM element it controls. This method is called exactly once at initialization with the control’s initial value, and again whenever the value changes programmatically .

registerOnChange(fn: any) is called by the forms API during setup, and it gives the accessor a callback function. The accessor stores this callback and calls it whenever the user changes the value. This is the view-to-model direction. The callback, when called with a new value, updates the FormControl‘s value, which triggers validation and propagates to the parent form .

registerOnTouched(fn: any) is called during setup and provides a callback that the accessor calls when the control has been “touched” — typically on blur. This is what makes control.touched work. Without it, the control never transitions from untouched to touched, and error messages that depend on touched never appear .

setDisabledState(isDisabled: boolean) is called when the control’s disabled status changes. The accessor should update the view to reflect the disabled state — for an input wrapper, this means setting the disabled attribute; for a custom widget, it means preventing interaction .

Why the callbacks are stored, not used immediately. registerOnChange and registerOnTouched are called once during initialization. The accessor stores the callbacks in instance properties (commonly named onChange and onTouched) and calls them later when user events fire. The initial values of these properties are no-ops — empty functions that do nothing — so that calling onChange before registration is safe .


Implementing a custom input component

The canonical example is a text input wrapper. It is the simplest custom control, and it shows the full pattern without the distraction of complex internal state.

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-custom-input',
  standalone: true,
  template: `
    <input
      [value]="value"
      (input)="onInput($event)"
      (blur)="onBlur()"
      [disabled]="disabled"
    />
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true,
    },
  ],
})
export class CustomInputComponent implements ControlValueAccessor {
  value = '';
  disabled = false;

  onChange: (value: string) => void = () => {};
  onTouched: () => void = () => {};

  writeValue(value: string): void {
    this.value = value ?? '';
  }

  registerOnChange(fn: (value: string) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  onInput(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.value = input.value;
    this.onChange(this.value);
  }

  onBlur(): void {
    this.onTouched();
  }
}

The providers array is the registration. The NG_VALUE_ACCESSOR token tells Angular that this component is a value accessor. The forwardRef is necessary because the component class is referenced inside its own decorator — TypeScript has not yet finished evaluating the class when the decorator runs, so the reference must be deferred . The multi: true flag allows multiple value accessors to exist in the same injector, which is how native elements and custom controls coexist .

Why useExisting and not useClass. useExisting tells Angular to use the existing component instance as the value accessor. useClass would create a new instance, separate from the one in the template. That new instance would have no connection to the DOM, no @Input bindings, and no state. The useExisting provider is the correct choice for every custom control .

How the methods work together. When the form initializes, Angular calls registerOnChange and registerOnTouched to set up the callbacks, then calls writeValue with the initial value. The writeValue method sets this.value, and the template binding [value]="value" updates the DOM. When the user types, onInput fires, updates this.value, and calls this.onChange(this.value), which updates the FormControl. When the user blurs, onBlur fires and calls this.onTouched(), which marks the control as touched .

Why the initial callbacks are no-ops. onChange and onTouched are initialized to empty functions. This is defensive: if onInput fires before registerOnChange has been called (which should not happen, but could in edge cases), calling this.onChange is still safe. The pattern is standard in every ControlValueAccessor implementation .


Registration and the NG_VALUE_ACCESSOR token

The provider registration is the mechanism that makes a component a value accessor. Without it, the component is just a component, and formControlName on it produces a runtime error: “No value accessor for form control with name: ‘…'” .

providers: [
  {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => MyCustomControl),
    multi: true,
  },
]

The token NG_VALUE_ACCESSOR is an injection token that Angular uses to look up value accessors. The multi: true flag means the token resolves to an array rather than a single value. When a form directive needs to find the value accessor for an element, it looks at all registered accessors and selects the appropriate one .

Why forwardRef is required. The decorator’s metadata is evaluated when the class is defined. At that moment, the class identifier MyCustomControl is not yet fully resolved — it is still being defined. forwardRef wraps the reference in a function that is called later, after the class is complete. Without it, the reference would be undefined at decoration time .

Why the selector matters. The component’s selector determines where the accessor is applied. A selector of 'app-custom-input' means the accessor is active on <app-custom-input> elements. A selector of '[myCustomControl]' means it is active on any element with the myCustomControl attribute. Both work; the choice is about usage ergonomics. For component-based controls, an element selector is common. For directive-based accessors that augment native elements, an attribute selector is common .

Why the component must import FormsModule or ReactiveFormsModule. The component itself uses [value] and (input) bindings, which are plain Angular bindings and do not require forms modules. But the parent that uses formControlName on the custom component needs ReactiveFormsModule (or FormsModule for ngModel). The custom control does not need to import them, but the parent does .


Adding validation to a custom control

A custom control can participate in validation in two ways: it can expose validators to the form model through the Validator interface, or it can handle validation internally and expose error state to its own template. Both are valid, and the choice depends on whether the validation rule belongs to the form or to the control.

Registering as a validator. A component can implement both ControlValueAccessor and Validator, registering itself with NG_VALIDATORS in addition to NG_VALUE_ACCESSOR.

providers: [
  {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CounterComponent),
    multi: true,
  },
  {
    provide: NG_VALIDATORS,
    useExisting: forwardRef(() => CounterComponent),
    multi: true,
  },
]

The Validator interface requires a validate method:

validate(control: AbstractControl): ValidationErrors | null {
  if (control.value < 0) {
    return { mustBePositive: { actual: control.value } };
  }
  return null;
}

The validate method is called by the forms API whenever the control’s value changes. Returning a non-null object makes the control invalid, and the error key becomes part of control.errors. The parent form’s validity reflects the custom control’s validity .

Why NG_VALIDATORS and NG_VALUE_ACCESSOR are separate. They serve different purposes. NG_VALUE_ACCESSOR is about getting values in and out. NG_VALIDATORS is about checking values. A control can be one, the other, or both. Registering both means the control handles its own value management and its own validation logic, which is the right split for a self-contained control like a counter that must never go negative .

Internal validation vs exposed validation. Some controls validate internally and display their own error messages. A password strength meter, for example, might check the password against rules and show a strength bar inside the control. In that case, the validation is a view concern, and the control does not need to register as a Validator. But if the validation should make the parent form invalid, the control must register with NG_VALIDATORS so the error propagates .

Why the control’s touched state matters for validation. Error messages typically appear only when control.touched is true. The control’s onTouched callback, registered through registerOnTouched, is what sets touched. If the custom control never calls onTouched, the parent form’s touched stays false, and error messages that depend on it never show. Calling onTouched on blur is the standard behavior .

Why a composite control needs care with validation. When a custom control wraps multiple inner fields, its validity is the conjunction of the inner fields’ validity. The validate method should return an error if any inner field is invalid. A common pattern is to give the outer component a Validator that checks the inner form’s validity and returns an error object if it is invalid .


Reactive forms and template-driven forms

ControlValueAccessor is form-agnostic. The same custom control works with formControlName in a reactive form and with [(ngModel)] in a template-driven form, without any changes .

In a reactive form:

<form [formGroup]="form">
  <app-custom-input formControlName="email"></app-custom-input>
</form>

In a template-driven form:

<form #f="ngForm">
  <app-custom-input name="email" [(ngModel)]="email"></app-custom-input>
</form>

The name attribute is required in template-driven forms for the same reason it is required on native inputs: it registers the control with NgForm. The custom control itself does not care — it only implements the ControlValueAccessor contract, and Angular handles the rest .

Why this matters for reuse. A custom control that only works with reactive forms is half a control. The value accessor pattern means the same component can be used in a simple template-driven login form and a complex reactive wizard without modification. This is the design goal, and it is why the interface exists .

Why testing benefits. A value accessor component can be tested in isolation by calling its methods directly: writeValue to set the view, onChange to simulate user input, onTouched to simulate blur. The form integration can be tested separately with a host component that uses formControlName. The separation makes both kinds of tests simpler .


Complete Example Session

import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, AbstractControl, ValidationErrors, Validator, FormsModule } from '@angular/forms';

// ============================================
// PART 1: THE CUSTOM CONTROL
// ============================================

@Component({
  selector: 'app-counter',
  standalone: true,
  imports: [FormsModule],
  template: `
    <div class="counter">
      <button type="button" (click)="decrement()" [disabled]="disabled">−</button>
      <span class="value">{{ value }}</span>
      <button type="button" (click)="increment()" [disabled]="disabled">+</button>
    </div>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CounterComponent),
      multi: true,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => CounterComponent),
      multi: true,
    },
  ],
})
export class CounterComponent implements ControlValueAccessor, Validator {
  @Input() step = 1;

  value = 0;
  disabled = false;

  private onChange: (value: number) => void = () => {};
  private onTouched: () => void = () => {};

  // --- ControlValueAccessor ---

  writeValue(value: number): void {
    this.value = value ?? 0;
  }

  registerOnChange(fn: (value: number) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  // --- Validator ---

  validate(control: AbstractControl): ValidationErrors | null {
    if (control.value < 0) {
      return { mustBePositive: { actual: control.value } };
    }
    return null;
  }

  // --- Interaction ---

  increment(): void {
    if (!this.disabled) {
      this.value += this.step;
      this.onChange(this.value);
      this.onTouched();
    }
  }

  decrement(): void {
    if (!this.disabled) {
      this.value -= this.step;
      this.onChange(this.value);
      this.onTouched();
    }
  }
}

The counter demonstrates the full pattern: writeValue sets the internal value, registerOnChange stores the callback, registerOnTouched stores the blur callback, setDisabledState updates the disabled flag, and validate checks that the value is not negative. The increment and decrement methods update the internal state, notify the form via onChange, and mark the control as touched via onTouched .


Quick Reference

The Four Methods

MethodDirectionCalled ByPurpose
writeValue(obj)Model → ViewForms APIUpdate view with new value
registerOnChange(fn)View → ModelForms APIStore callback for value changes
registerOnTouched(fn)View → ModelForms APIStore callback for blur
setDisabledState(bool)Model → ViewForms APIUpdate disabled state

Provider Registration

ProviderTokenPurpose
Value accessorNG_VALUE_ACCESSORRegister as form control
ValidatorNG_VALIDATORSRegister as validator
multiMust be true
forwardRefRequired for self-reference
useExistingUse the component instance

Built-in Value Accessors

AccessorElement
DefaultValueAccessorinput[text], textarea
CheckboxControlValueAccessorinput[type=checkbox]
NumberValueAccessorinput[type=number]
RadioControlValueAccessorinput[type=radio]
RangeValueAccessorinput[type=range]
SelectControlValueAccessorselect

Usage in Forms

Form TypeTemplate
Reactive<my-control formControlName="x">
Template-driven<my-control name="x" [(ngModel)]="x">

Common Patterns

PatternPurpose
value = ''Internal view state
onChange: (v) => void = () => {}Stored callback
onTouched: () => void = () => {}Stored blur callback
disabled = falseInternal disabled state
(input)="onInput($event)"View-to-model trigger
(blur)="onTouched()"Touch notification

Best Practices

Do This:

// Use useExisting with forwardRef
providers: [
  {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => MyControl),
    multi: true,
  },
]                                                              // ✅

// Initialize callbacks as no-ops
onChange: (v: any) => void = () => {};
onTouched: () => void = () => {};                              // ✅

// Call onChange when the value changes
onInput(event: Event) {
  this.value = (event.target as HTMLInputElement).value;
  this.onChange(this.value);
}                                                              // ✅

// Call onTouched on blur
onBlur() { this.onTouched(); }                                 // ✅

// Implement setDisabledState
setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } // ✅

Don’t Do This:

// Don't use useClass — creates a separate instance
providers: [{ provide: NG_VALUE_ACCESSOR, useClass: MyControl, multi: true }] // ⚠️

// Don't forget multi: true
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: MyControl }] // ⚠️

// Don't call onChange without updating internal state
onInput(e) { this.onChange(e.target.value); } // ⚠️ view not updated

// Don't skip registerOnTouched
// Without it, touched never becomes true                          // ⚠️

// Don't forget forwardRef
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: MyControl, multi: true }] // ⚠️

Common Pitfalls

PitfallProblemSolution
useClass instead of useExistingSeparate instance, no bindingsUse useExisting
Missing multi: trueOnly one accessor registeredAdd multi: true
Missing forwardRefundefined referenceWrap in forwardRef
Callbacks not initializedRuntime error on early callInitialize as no-ops
onTouched never calledError messages never showCall on blur
writeValue not updating viewModel changes not reflectedSet internal state in writeValue
setDisabledState missingDisable ignoredImplement optional method
Validator not registeredValidation not propagatedAdd NG_VALIDATORS provider

Real-World Examples

1. Star rating

@Component({ selector: 'app-rating', /* ... */ })
export class RatingComponent implements ControlValueAccessor {
  rating = 0;
  writeValue(value: number) { this.rating = value; }
  registerOnChange(fn: any) { this.onChange = fn; }
  registerOnTouched(fn: any) { this.onTouched = fn; }
  setRating(value: number) { this.rating = value; this.onChange(value); }
}

2. Color picker

@Component({ selector: 'app-color', /* ... */ })
export class ColorPickerComponent implements ControlValueAccessor {
  color = '#000000';
  writeValue(value: string) { this.color = value; }
  onColorChange(value: string) { this.color = value; this.onChange(value); }
}

3. Address composite

@Component({ selector: 'app-address', /* ... */ })
export class AddressComponent implements ControlValueAccessor {
  address = { street: '', city: '', zip: '' };
  writeValue(value: any) { this.address = value; }
  onFieldChange() { this.onChange({ ...this.address }); }
}

4. Counter with validation

validate(control: AbstractControl) {
  return control.value < 0 ? { mustBePositive: true } : null;
}

5. Toggle switch

@Component({ selector: 'app-toggle', /* ... */ })
export class ToggleComponent implements ControlValueAccessor {
  checked = false;
  writeValue(value: boolean) { this.checked = value; }
  toggle() { this.checked = !this.checked; this.onChange(this.checked); }
}

6. Rich text editor wrapper

@Component({ selector: 'app-editor', /* ... */ })
export class EditorComponent implements ControlValueAccessor {
  content = '';
  writeValue(value: string) { this.content = value; }
  onContentChange(value: string) { this.content = value; this.onChange(value); }
}

7. File upload with progress

@Component({ selector: 'app-file-upload', /* ... */ })
export class FileUploadComponent implements ControlValueAccessor {
  fileName = '';
  writeValue(value: string) { this.fileName = value; }
  onFileSelected(file: File) { this.fileName = file.name; this.onChange(file.name); }
}

8. Slider with value display

@Component({ selector: 'app-slider', /* ... */ })
export class SliderComponent implements ControlValueAccessor {
  value = 0;
  writeValue(value: number) { this.value = value; }
  onSlide(value: number) { this.value = value; this.onChange(value); }
}

9. Date picker wrapper

@Component({ selector: 'app-date', /* ... */ })
export class DatePickerComponent implements ControlValueAccessor {
  date: Date | null = null;
  writeValue(value: Date) { this.date = value; }
  onDateSelected(date: Date) { this.date = date; this.onChange(date); }
}

10. Composite phone number

@Component({ selector: 'app-phone', /* ... */ })
export class PhoneComponent implements ControlValueAccessor {
  parts = { country: '', area: '', number: '' };
  writeValue(value: any) { this.parts = value; }
  onPartChange() { this.onChange({ ...this.parts }); }
}

Visual: ControlValueAccessor Bridge

┌──────────────────────────────────────────────────────────┐
│                                                          │
│   FormControl (model)                                    │
│       │                                                  │
│       │  setValue("new")                                  │
│       ▼                                                  │
│   ┌─────────────────────────────────────┐                │
│   │  ControlValueAccessor               │                │
│   │                                     │                │
│   │  writeValue("new")  ──────────────► │                │
│   │                                     │                │
│   │  ◄────────────── onChange("typed")  │                │
│   │                                     │                │
│   │  ◄────────────── onTouched()        │                │
│   │                                     │                │
│   └─────────────────────────────────────┘                │
│       │                                                  │
│       │                                                  │
│       ▼                                                  │
│   Custom Component (view)                                │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Four Methods

┌──────────────────────────────────────────────────────────┐
│  Model → View                                            │
│    writeValue(value)                                     │
│      Called when the form model changes.                 │
│      Accessor updates its internal state / DOM.          │
│                                                          │
│    setDisabledState(bool)                                │
│      Called when disabled status changes.                │
│      Accessor updates disabled state.                    │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  View → Model                                            │
│    registerOnChange(fn)                                  │
│      Called once during setup.                           │
│      Accessor stores fn and calls it on user input.      │
│                                                          │
│    registerOnTouched(fn)                                 │
│      Called once during setup.                           │
│      Accessor stores fn and calls it on blur.            │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Registration with forwardRef

┌──────────────────────────────────────────────────────────┐
│  @Component({                                            │
│    selector: 'app-custom',                               │
│    providers: [{                                         │
│      provide: NG_VALUE_ACCESSOR,                         │
│      useExisting: forwardRef(() => CustomComponent),     │
│      multi: true,                                        │
│    }]                                                    │
│  })                                                      │
│  export class CustomComponent                            │
│    implements ControlValueAccessor { ... }               │
│                                                          │
│  forwardRef defers the reference until after the         │
│  class is fully defined.                                 │
│                                                          │
│  Without forwardRef: CustomComponent is undefined.       │
│  With forwardRef: CustomComponent resolves correctly.    │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Custom Control in a Form

┌──────────────────────────────────────────────────────────┐
│  <form [formGroup]="form">                               │
│    <input formControlName="name" />                      │
│                                                          │
│    <app-counter formControlName="count" />               │
│         │                                                │
│         └── Custom component                             │
│              implements ControlValueAccessor             │
│              registered with NG_VALUE_ACCESSOR           │
│                                                          │
│    <app-toggle formControlName="active" />               │
│  </form>                                                 │
│                                                          │
│  Angular treats all three controls identically.          │
│  The form model is unaware which are native and which    │
│  are custom.                                             │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Validation Flow

┌──────────────────────────────────────────────────────────┐
│  User clicks "+" on counter                              │
│       │                                                  │
│       ▼                                                  │
│  increment() called                                      │
│       │                                                  │
│       ▼                                                  │
│  this.value += step                                      │
│       │                                                  │
│       ▼                                                  │
│  this.onChange(this.value)                               │
│       │                                                  │
│       ▼                                                  │
│  FormControl.value = new value                           │
│       │                                                  │
│       ▼                                                  │
│  FormControl runs validators                             │
│       │                                                  │
│       ▼                                                  │
│  validate() called on custom control                     │
│       │                                                  │
│       ├── value >= 0 ──► return null (valid)             │
│       │                                                  │
│       └── value < 0  ──► return { mustBePositive }       │
│                                                          │
│  Form validity updates automatically.                    │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
InterfaceControlValueAccessor
Required methodswriteValue, registerOnChange, registerOnTouched
Optional methodsetDisabledState
Registration tokenNG_VALUE_ACCESSOR
Validator tokenNG_VALIDATORS
ReferenceforwardRef(() => Component)
Provider typeuseExisting, multi: true
Direction model → viewwriteValue
Direction view → modelonChange, onTouched
Works withReactive and template-driven forms

Key takeaways:

  • ControlValueAccessor is the bridge between a custom component and Angular’s forms API — implementing it makes the component indistinguishable from a native input to formControlName, ngModel, and the validation system
  • The interface has four methods: writeValue for model-to-view updates, registerOnChange for view-to-model notifications, registerOnTouched for blur tracking, and setDisabledState for disabled handling
  • Registration requires the NG_VALUE_ACCESSOR token with useExisting, forwardRef, and multi: true — all three are mandatory and serve distinct purposes
  • Callbacks must be initialized as no-ops and stored when registerOnChange and registerOnTouched are called
  • Validation is opt-in via NG_VALIDATORS — a control can implement both interfaces to participate in both value management and validation
  • The pattern is form-agnostic — the same component works in reactive and template-driven forms without changes
  • writeValue is called on initialization and programmatic changes, not on user input — user input flows through onChange
  • onTouched must be called on blur or the control never transitions to touched, and error messages that depend on it never appear
  • Custom controls are first-class form citizens — their validity, touched, dirty, and disabled states all propagate to the parent form automatically

Remember: ControlValueAccessor turns a component into a form control. The four methods are the entire contract, and once they are implemented and the component is registered, Angular treats it exactly like an <input>. The pattern is uniform across every custom control, from a simple text wrapper to a complex composite widget, and it works in every form type. Master the four methods and the registration, and the rest is component-specific logic.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!