skip to content

In Angular, why does formControlName on a custom <app-star-rating> component throw 'No value accessor for form control', and how do you fix it?

level: juniorimportance: should knowfreq 50%

answer

  1. the directive needs a bridge
  2. looked up on the same element
  3. a token with multi: true
  4. useExisting, not useClass

basics

~20 s

formControlName looks for an NG_VALUE_ACCESSOR on its own element. A plain component provides none and the built-in accessors target native inputs, so Angular throws NG01203. Implement ControlValueAccessor and provide NG_VALUE_ACCESSOR with useExisting, forwardRef and multi: true.

solid answer

~40 s

`formControlName`, `[formControl]` and `ngModel` inject the `NG_VALUE_ACCESSOR` token from **their own element only**. On `<app-star-rating>` there is nothing: the built-in accessors match native `input`, `textarea` and `select` elements (or an element marked `ngDefaultControl`), so Angular throws `NG01203` "No value accessor for form control name: 'rating'". The fix is to implement `ControlValueAccessor` and add `{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarRating), multi: true}` to the component's `providers`. `useExisting` makes the token resolve to the rendered instance, and forgetting `multi: true` produces a different dev-mode error, `NG01200`, telling you the accessor was not provided as an array. For a third-party element that already behaves like a text input, `ngDefaultControl` is the escape hatch.

go deeper

for a junior

Recognise NG01203 as a missing accessor, not a missing FormControl, and write the provider with useExisting, forwardRef and multi: true from memory.

for a middle

Explain that the directive injects the token from its own element only, why useExisting rather than useClass, and what error appears when multi: true is left out.

for a senior

Describe how Angular chooses among several accessors on one element and how to reach NgControl from inside an accessor without a circular dependency.

for a principal

Judge whether a design system should wrap third-party inputs with its own accessors or rely on ngDefaultControl, weighing consistency of touched and disabled behaviour against maintenance.

## What the error means Every forms directive that binds a single control (`formControlName`, `[formControl]`, `ngModel`) needs a **value accessor**: an object implementing `ControlValueAccessor` that knows how to write a value into the element and how to report user edits. The directive does not create one. It asks dependency injection for the multi-provider token **`NG_VALUE_ACCESSOR`**, and it asks with the `@Self()` restriction, meaning only providers on **the same element** count. Nothing from a parent component or from the application root is considered. If that lookup returns nothing, the directive cannot connect the `FormControl` to the element, and in development mode Angular throws runtime error **`NG01203`**: ```text NG01203: No value accessor for form control name: 'rating'. ``` ## Why a custom component has no accessor Angular ships accessors for native elements only: | Accessor | Matches (simplified) | |---|---| | `DefaultValueAccessor` | text-like `input` and `textarea` with a forms directive, or any element with `ngDefaultControl` | | `CheckboxControlValueAccessor` | `input[type=checkbox]` | | `NumberValueAccessor`, `RangeValueAccessor` | `input[type=number]`, `input[type=range]` | | `RadioControlValueAccessor` | `input[type=radio]` | | `SelectControlValueAccessor` and its multiple variant | `select` | `<app-star-rating>` matches none of these selectors, so no accessor lands on the element. Implementing the interface is not enough on its own either: Angular never scans classes for `implements ControlValueAccessor` (TypeScript interfaces do not exist at runtime). The component has to **register** itself under the token. ## The fix ```ts @Component({ selector: 'app-star-rating', providers: [ {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarRating), multi: true}, ], template: `...`, }) export class StarRating implements ControlValueAccessor { /* four methods */ } ``` Each part of the provider matters: - **`useExisting`** aliases the token to the component instance that is already on the element. `useClass` would build a *second* `StarRating` object that is never rendered, so `writeValue` would update an invisible copy. - **`forwardRef`** is needed because the class is referenced inside its own decorator, before the class binding exists. - **`multi: true`** is required because the directive reads the token as an array of candidates. Without it Angular reports, in development mode, **`NG01200`**: the value accessor was not provided as an array and the token should be a `multi: true` provider. ## Other causes listed for NG01203 The error page in the Angular docs names several less obvious triggers: 1. `ngModel` placed on an element that has no value at all, such as `<div [(ngModel)]="x">`. 2. A custom control declared in an `NgModule` that the using module does not import. 3. A third-party control that provides no accessor; `ngDefaultControl` tells Angular to attach `DefaultValueAccessor`, which writes the element's `value` property and listens for `input` and `blur`. 4. A unit test whose testing module does not know the control. ## How Angular picks between several accessors The token is multi-valued, so an element can collect more than one accessor. The directive then chooses with a fixed priority: 1. a **custom** accessor (anything not shipped by `@angular/forms`) wins; 2. otherwise a **built-in** one such as the checkbox or select accessor; 3. otherwise **`DefaultValueAccessor`**. Two custom accessors on the same element, or two built-in ones, are ambiguous and, in development mode, throw "More than one custom value accessor matches form control". This priority is why a custom directive placed on a native `<input>` can take over from the default accessor without any extra configuration. ## Why the lookup is element-local Restricting the lookup to the directive's own element is deliberate. A form can contain dozens of controls, each with its own accessor, nested inside components that are themselves custom controls. If `formControlName` walked up the injector tree, a field inside a composite widget could pick up the widget's accessor instead of its own, and the binding would silently attach to the wrong element. Keeping the provider in the component's `providers` array (not `viewProviders`, and not a parent's) is what ties one accessor to one control directive on one element. ## Checklist when you see NG01203 - Does the component implement `ControlValueAccessor` **and** list the provider in `providers` of the component itself? - Is the provider `multi: true` and `useExisting`? - Is the component actually imported where it is used (standalone `imports`, or the declaring `NgModule`)? - Is the forms directive on the component's own tag rather than on a wrapper element?

  • Your accessor also needs to read the control's errors, so you inject NgControl into it and get a circular dependency. What is the usual fix?
    Providing `NG_VALUE_ACCESSOR` makes the directive depend on the component while the component depends on `NgControl`, which is a cycle. Drop the provider, inject `NgControl` with `{self: true, optional: true}` and assign `ngControl.valueAccessor = this` in the constructor. The forms directives keep a value accessor that was set this way instead of selecting one from the token.
  • Why does a custom accessor directive on an <input> not clash with DefaultValueAccessor?
    Both end up in the `NG_VALUE_ACCESSOR` array, but the directive selects by priority: a custom accessor beats a built-in one, which beats the default. Only two accessors of the same custom (or built-in) rank are an error.

saying these in an interview costs you the question

  • Implementing the ControlValueAccessor interface alone is enough for Angular to find it
  • useClass and useExisting are interchangeable in the NG_VALUE_ACCESSOR provider
  • Providing NG_VALUE_ACCESSOR once in the root injector covers every custom control
  • multi: true is optional when the element has only one accessor
  • The error means the FormControl itself is missing from the FormGroup