Skip to the solution
MediumFrontend9 min read

What is the role of NG_VALUE_ACCESSOR in a custom component?

879 views

The problem

Question presented to candidate: "Every custom form control in our codebase has this providers line: provide NG_VALUE_ACCESSOR, useExisting forwardRef, multi true. A junior developer asks what it actually does and whether they can delete parts of it. What do you tell them?"

What a strong answer should cover:

  • NG_VALUE_ACCESSOR is an InjectionToken that forms directives (NgModel, FormControlDirective, FormControlName) inject from the same element to find the object implementing ControlValueAccessor.
  • multi: true: several directives on one element can contribute accessors (the built-in DefaultValueAccessor also matches many elements); Angular collects them into an array and picks one: custom beats built-in beats default. Without multi, Angular throws NG01200.
  • useExisting: reuse the component instance Angular already created, instead of constructing a second one (useClass would).
  • forwardRef: defers reading the class reference; it is the documented convention and needed when the class is referenced before its declaration is initialised. Verified: on 22.2.3 a self-reference in the class's own decorator also worked without it.
  • No provider at all: NG01203: No value accessor for form control .... Two custom accessors on one element: "More than one custom value accessor matches".
  • Alternative: inject NgControl and set ngControl.valueAccessor = this (no provider; providing both creates a circular dependency, NG0200). In v22, model()-based controls need no token.

Clarifying questions expected:

  • "Does the component also need to display its own validation errors?"
  • "Is it used with both reactive and template-driven forms?"

Code / implementation expected: Yes β€” the provider line explained, plus the NgControl-injection alternative.

  • forms
  • di

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

AngularTwo registration styles: NG_VALUE_ACCESSOR multi-provider and injecting NgControl directly
import { Component, forwardRef, inject, signal } from "@angular/core";
import {
  ControlValueAccessor,
  FormControl,
  FormGroup,
  NG_VALUE_ACCESSOR,
  NgControl,
  ReactiveFormsModule,
  Validators,
} from "@angular/forms";

// Pattern A: register through the NG_VALUE_ACCESSOR multi-provider token.
@Component({
  selector: "yes-no",
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => YesNo), multi: true }],
  template: `<button type="button" class="yes-no" (click)="flip()">{{ on() ? "yes" : "no" }}</button>`,
})
export class YesNo implements ControlValueAccessor {
  on = signal(false);
  private onChange: (v: boolean) => void = () => {};
  writeValue(v: boolean | null) { this.on.set(!!v); }
  registerOnChange(fn: (v: boolean) => void) { this.onChange = fn; }
  registerOnTouched() {}
  flip() { this.on.update((x) => !x); this.onChange(this.on()); }
}

// Pattern B: no provider. Inject the NgControl on the same element and hand it this object.
// Avoids the circular dependency you get if you provide NG_VALUE_ACCESSOR AND inject NgControl,
// and gives the component direct access to the control (errors, touched...).
@Component({
  selector: "code-input",
  template: `
    <input class="code" [value]="value()" (input)="onInput($any($event.target).value)" (blur)="onTouched()" />
    @if (ngControl?.touched && ngControl?.errors) {
      <small class="code-err">{{ errorText() }}</small>
    }
  `,
})
export class CodeInput implements ControlValueAccessor {
  ngControl = inject(NgControl, { self: true, optional: true });
  value = signal("");
  private onChange: (v: string) => void = () => {};
  onTouched: () => void = () => {};

  constructor() {
    if (this.ngControl) this.ngControl.valueAccessor = this;
  }
  writeValue(v: string | null) { this.value.set(v ?? ""); }
  registerOnChange(fn: (v: string) => void) { this.onChange = fn; }
  registerOnTouched(fn: () => void) { this.onTouched = fn; }
  onInput(v: string) {
    const upper = v.toUpperCase(); // the accessor may transform the value on the way to the model
    this.value.set(upper);
    this.onChange(upper);
  }
  errorText() {
    return Object.keys(this.ngControl?.errors ?? {}).join(", ");
  }
}

@Component({
  selector: "app-root",
  imports: [ReactiveFormsModule, YesNo, CodeInput],
  template: `
    <form [formGroup]="form">
      <yes-no formControlName="agree" />
      <code-input formControlName="code" />
    </form>
    <p id="out">{{ out() }}</p>
  `,
})
export class AppComponent {
  form = new FormGroup({
    agree: new FormControl(false, { nonNullable: true, validators: Validators.requiredTrue }),
    code: new FormControl("", { nonNullable: true, validators: [Validators.required, Validators.pattern(/^[A-Z]{3}$/)] }),
  });
  out = signal(JSON.stringify(this.form.value) + " " + this.form.status);
  constructor() {
    this.form.valueChanges.subscribe((v) => this.out.set(JSON.stringify(v) + " " + this.form.status));
  }
}

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 27 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 nextMedium

inject() vs constructor injection β€” when should you use each?

Continue