Skip to the solution
HardFrontend9 min read

How do you build a custom form control by implementing the ControlValueAccessor interface?

994 views

The problem

Question presented to candidate: "Design wants a five-star rating widget that behaves like a normal input inside our reactive forms: it should take a value from the FormControl, report changes, mark the control touched, and respect disable(). How do you build it with ControlValueAccessor?"

What a strong answer should cover:

  • ControlValueAccessor (CVA) is the adapter interface between a forms directive (formControl, formControlName, ngModel) and any UI element.
  • The four methods and their direction: writeValue (model to view), registerOnChange and registerOnTouched (store callbacks for view to model), optional setDisabledState.
  • Register it with { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarRating), multi: true }.
  • Call the stored onChange only for user changes, never inside writeValue; call onTouched on blur.
  • Know the call order: reactive directives call writeValue first, then setDisabledState (always, since v15's default), then the register methods; ngModel first writes null, then the real value a tick later.
  • Handle null (from reset()), and keep internal state in signals so the OnPush view updates. Bonus: Angular 22 can also drive a CVA-free model()-based control.

Clarifying questions expected:

  • "Should the widget support keyboard input and be focusable for accessibility?"
  • "What should the value be when the form is reset: 0 or null?"

Code / implementation expected: Yes β€” a working CVA component used with a FormControl, ideally logging when each method is called.

  • forms
  • custom-control

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 10 sections

Read the code

AngularA star-rating ControlValueAccessor that logs every call the forms API makes
import { Component, forwardRef, signal } from "@angular/core";
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators } from "@angular/forms";

@Component({
  selector: "star-rating",
  providers: [
    // Tells the forms directives on <star-rating> which object speaks the ControlValueAccessor protocol.
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarRating), multi: true },
  ],
  template: `
    @for (star of stars; track star) {
      <button type="button" class="star" [disabled]="disabled()" (click)="pick(star)" (blur)="onTouched()">
        {{ star <= value() ? "β˜…" : "β˜†" }}
      </button>
    }
  `,
})
export class StarRating implements ControlValueAccessor {
  stars = [1, 2, 3, 4, 5];
  value = signal(0);
  disabled = signal(false);
  private onChange: (value: number) => void = () => {};
  onTouched: () => void = () => {};

  // MODEL -> VIEW: the form pushes a value in. Do not call onChange here.
  writeValue(value: number | null): void {
    console.log("writeValue(" + value + ")");
    this.value.set(value ?? 0);
  }
  // VIEW -> MODEL: the form hands us a callback to call when the user changes the value.
  registerOnChange(fn: (value: number) => void): void {
    console.log("registerOnChange");
    this.onChange = fn;
  }
  registerOnTouched(fn: () => void): void {
    console.log("registerOnTouched");
    this.onTouched = fn;
  }
  setDisabledState(isDisabled: boolean): void {
    console.log("setDisabledState(" + isDisabled + ")");
    this.disabled.set(isDisabled);
  }

  pick(star: number) {
    this.value.set(star);
    this.onChange(star);
  }
}

@Component({
  selector: "app-root",
  imports: [ReactiveFormsModule, StarRating],
  template: `
    <star-rating [formControl]="rating" />
    <button id="toggle" (click)="toggle()">toggle disabled</button>
    <button id="reset" (click)="rating.setValue(2)">setValue(2)</button>
    <p id="out">value={{ value() }}</p>
  `,
})
export class AppComponent {
  rating = new FormControl(3, { validators: Validators.min(1) });
  value = signal(this.rating.value);
  constructor() {
    this.rating.valueChanges.subscribe((v) => this.value.set(v));
  }
  toggle() {
    if (this.rating.disabled) this.rating.enable();
    else this.rating.disable();
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 88 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

How does change detection work in Angular?

Continue