Skip to content

Commit

Permalink
fix(forms): Improve a very commonly viewed error message by adding a …
Browse files Browse the repository at this point in the history
…guide. (#47969)

[A Github issue](#43821) about an arcane-sounding Forms error is one of the repo's top-ten most visited pages. This converts the error to `RuntimeErrorCode` and adds a dedicated guide to explain how to solve it.

PR Close #47969
  • Loading branch information
dylhunn authored and AndrewKushnir committed Nov 8, 2022
1 parent 917816f commit 604cdb7
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 9 deletions.
28 changes: 28 additions & 0 deletions aio/content/errors/NG1203.md
@@ -0,0 +1,28 @@
@name Missing value accessor
@category forms
@shortDescription You must register an `NgValueAccessor` with a custom form control

@description
For all custom form controls, you must register a value accessor.

Here's an example of how to provide one:

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

@debugging
As described above, your control was expected to have a value accessor, but was missing one. However, there are many different reasons this can happen in practice. Here's a listing of some known problems leading to this error.

1. If you **defined** a custom form control, did you remember to provide a value accessor?
1. Did you put `ngModel` on an element with no value, or an **invalid element** (e.g. `<div [(ngModel)]="foo">`)?
1. Are you using a custom form control declared inside an `NgModule`? if so, make sure you are **importing** the `NgModule`.
1. Are you using `ngModel` with a third-party custom form control? Check whether that control provides a value accessor. If not, use **`ngDefaultControl`** on the control's element.
1. Are you **testing** a custom form control? Be sure to configure your testbed to know about the control. You can do so with `Testbed.configureTestingModule`.
1. Are you using **Nx and Module Federation** with Webpack? Your `webpack.config.js` may require [extra configuration](https://github.com/angular/angular/issues/43821#issuecomment-1054845431) to ensure the forms package is shared.
2 changes: 2 additions & 0 deletions goldens/public-api/forms/errors.md
Expand Up @@ -25,6 +25,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
NAME_AND_FORM_CONTROL_NAME_MUST_MATCH = 1202,
// (undocumented)
NG_MISSING_VALUE_ACCESSOR = -1203,
// (undocumented)
NG_VALUE_ACCESSOR_NOT_PROVIDED = 1200,
// (undocumented)
NGMODEL_IN_FORM_GROUP = 1350,
Expand Down
4 changes: 2 additions & 2 deletions goldens/size-tracking/integration-payloads.json
Expand Up @@ -48,8 +48,8 @@
"animations": {
"uncompressed": {
"runtime": 1070,
"main": 155920,
"polyfills": 33814
"main": 156355,
"polyfills": 33897
}
},
"standalone-bootstrap": {
Expand Down
8 changes: 7 additions & 1 deletion packages/forms/src/directives/shared.ts
Expand Up @@ -65,7 +65,7 @@ export function setUpControl(
callSetDisabledState: SetDisabledStateOption = setDisabledStateDefault): void {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
if (!dir.valueAccessor) _throwMissingValueAccessorError(dir);
}

setUpValidators(control, dir);
Expand Down Expand Up @@ -322,6 +322,12 @@ function _describeControlLocation(dir: AbstractControlDirective): string {
return 'unspecified name attribute';
}

function _throwMissingValueAccessorError(dir: AbstractControlDirective) {
const loc = _describeControlLocation(dir);
throw new RuntimeError(
RuntimeErrorCode.NG_MISSING_VALUE_ACCESSOR, `No value accessor for form control ${loc}.`);
}

function _throwInvalidValueAccessorError(dir: AbstractControlDirective) {
const loc = _describeControlLocation(dir);
throw new RuntimeError(
Expand Down
1 change: 1 addition & 0 deletions packages/forms/src/errors.ts
Expand Up @@ -31,6 +31,7 @@ export const enum RuntimeErrorCode {
NG_VALUE_ACCESSOR_NOT_PROVIDED = 1200,
COMPAREWITH_NOT_A_FN = 1201,
NAME_AND_FORM_CONTROL_NAME_MUST_MATCH = 1202,
NG_MISSING_VALUE_ACCESSOR = -1203,

// Template-driven Forms errors (1350-1399)
NGMODEL_IN_FORM_GROUP = 1350,
Expand Down
12 changes: 7 additions & 5 deletions packages/forms/test/directives_spec.ts
Expand Up @@ -184,7 +184,8 @@ class CustomValidatorDirective implements Validator {
dir.name = 'login';

expect(() => form.addControl(dir))
.toThrowError(new RegExp(`No value accessor for form control with name: 'login'`));
.toThrowError(new RegExp(
`NG01203: No value accessor for form control name: 'login'. Find more at https://angular.io/errors/NG01203`));
});

it('should throw when no value accessor with path', () => {
Expand All @@ -195,7 +196,7 @@ class CustomValidatorDirective implements Validator {

expect(() => form.addControl(dir))
.toThrowError(new RegExp(
`No value accessor for form control with path: 'passwords -> password'`));
`NG01203: No value accessor for form control path: 'passwords -> password'. Find more at https://angular.io/errors/NG01203`));
});

it('should set up validators', fakeAsync(() => {
Expand Down Expand Up @@ -582,15 +583,16 @@ class CustomValidatorDirective implements Validator {
namedDir.name = 'one';

expect(() => namedDir.ngOnChanges({}))
.toThrowError(new RegExp(`No value accessor for form control with name: 'one'`));
.toThrowError(new RegExp(
`NG01203: No value accessor for form control name: 'one'. Find more at https://angular.io/errors/NG01203`));
});

it('should throw when no value accessor with unnamed control', () => {
const unnamedDir = new NgModel(null!, null!, null!, null!);

expect(() => unnamedDir.ngOnChanges({}))
.toThrowError(
new RegExp(`No value accessor for form control with unspecified name attribute`));
.toThrowError(new RegExp(
`NG01203: No value accessor for form control unspecified name attribute. Find more at https://angular.io/errors/NG01203`));
});

it('should set up validator', fakeAsync(() => {
Expand Down
4 changes: 3 additions & 1 deletion packages/forms/test/reactive_integration_spec.ts
Expand Up @@ -5093,7 +5093,9 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
const fixture = initTest(NoCVAComponent);
expect(() => {
fixture.detectChanges();
}).toThrowError('No value accessor for form control with name: \'control\'');
})
.toThrowError(
`NG01203: No value accessor for form control name: 'control'. Find more at https://angular.io/errors/NG01203`);

// Making sure that cleanup between tests doesn't cause any issues
// for not fully initialized controls.
Expand Down

0 comments on commit 604cdb7

Please sign in to comment.