diff --git a/aio/content/errors/NG1203.md b/aio/content/errors/NG1203.md new file mode 100644 index 0000000000000..4629b34ea1e9b --- /dev/null +++ b/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. `
`)? +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. diff --git a/goldens/public-api/forms/errors.md b/goldens/public-api/forms/errors.md index c10ea38d9c097..182184a8276b8 100644 --- a/goldens/public-api/forms/errors.md +++ b/goldens/public-api/forms/errors.md @@ -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, diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 5d6632ba4f543..7a7418127ac9c 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -48,8 +48,8 @@ "animations": { "uncompressed": { "runtime": 1070, - "main": 155920, - "polyfills": 33814 + "main": 156355, + "polyfills": 33897 } }, "standalone-bootstrap": { diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 502dce2da1f67..454ade79df34c 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -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); @@ -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( diff --git a/packages/forms/src/errors.ts b/packages/forms/src/errors.ts index 868f761a02c06..003c07939cced 100644 --- a/packages/forms/src/errors.ts +++ b/packages/forms/src/errors.ts @@ -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, diff --git a/packages/forms/test/directives_spec.ts b/packages/forms/test/directives_spec.ts index 18c88fb1a3cd0..39897599e5db2 100644 --- a/packages/forms/test/directives_spec.ts +++ b/packages/forms/test/directives_spec.ts @@ -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', () => { @@ -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(() => { @@ -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(() => { diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 7a0bc61fee504..e783ec37d80fd 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -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.