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.