From 06d856ee0ed381655d437fc5c9f0ca770c1b9cd5 Mon Sep 17 00:00:00 2001 From: Amer Yousuf Date: Thu, 24 Feb 2022 17:01:19 +0300 Subject: [PATCH 1/2] fix(forms): improve error message for invalid value accessors improve error message for invalid value accessors when accessor is not provided as array --- packages/forms/src/directives/shared.ts | 12 ++++++++---- packages/forms/test/directives_spec.ts | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 377965f492ac..efba3cf98a21 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -275,16 +275,18 @@ function _noControlError(dir: NgControl) { return _throwError(dir, 'There is no FormControl instance attached to form control element with'); } -function _throwError(dir: AbstractControlDirective, message: string): void { +function _throwError(dir: AbstractControlDirective, message: string, extraMessage?: string): void { let messageEnd: string; if (dir.path!.length > 1) { messageEnd = `path: '${dir.path!.join(' -> ')}'`; } else if (dir.path![0]) { messageEnd = `name: '${dir.path}'`; } else { - messageEnd = 'unspecified name attribute'; + messageEnd = 'unspecified name attribute.'; } - throw new Error(`${message} ${messageEnd}`); + + extraMessage = extraMessage ? '\n' + extraMessage : ''; + throw new Error(`${message} ${messageEnd}${extraMessage}`); } export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any): boolean { @@ -318,7 +320,9 @@ export function selectValueAccessor( if (!valueAccessors) return null; if (!Array.isArray(valueAccessors) && (typeof ngDevMode === 'undefined' || ngDevMode)) - _throwError(dir, 'Value accessor was not provided as an array for form control with'); + _throwError( + dir, 'Value accessor was not provided as an array for form control with', + 'Please make sure that the `NG_VALUE_ACCESSOR` token is configured as a multi provider (with the `multi: true` property).'); let defaultAccessor: ControlValueAccessor|undefined = undefined; let builtinAccessor: ControlValueAccessor|undefined = undefined; diff --git a/packages/forms/test/directives_spec.ts b/packages/forms/test/directives_spec.ts index 0b94bdc02bdf..28cb2c704eee 100644 --- a/packages/forms/test/directives_spec.ts +++ b/packages/forms/test/directives_spec.ts @@ -11,6 +11,7 @@ import {fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {AbstractControl, CheckboxControlValueAccessor, ControlValueAccessor, DefaultValueAccessor, FormArray, FormArrayName, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormGroupName, NgControl, NgForm, NgModel, NgModelGroup, SelectControlValueAccessor, SelectMultipleControlValueAccessor, ValidationErrors, Validator, Validators} from '@angular/forms'; import {selectValueAccessor} from '@angular/forms/src/directives/shared'; import {composeValidators} from '@angular/forms/src/validators'; + import {asyncValidator} from './util'; class DummyControlValueAccessor implements ControlValueAccessor { @@ -53,7 +54,7 @@ class CustomValidatorDirective implements Validator { it('should throw when accessor is not provided as array', () => { expect(() => selectValueAccessor(dir, {} as any[])) .toThrowError( - `Value accessor was not provided as an array for form control with unspecified name attribute`); + 'Value accessor was not provided as an array for form control with unspecified name attribute.\nPlease make sure that the `NG_VALUE_ACCESSOR` token is configured as a multi provider (with the `multi: true` property).'); }); it('should return the default value accessor when no other provided', () => { From 158a7d43c324f2b486d7054e91f6835dc2827e6d Mon Sep 17 00:00:00 2001 From: Amer Yousuf Date: Sun, 13 Mar 2022 19:34:35 +0300 Subject: [PATCH 2/2] fixup! fix(forms): improve error message for invalid value accessors --- packages/forms/src/directives/shared.ts | 40 ++++++++++++------------- packages/forms/test/directives_spec.ts | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index efba3cf98a21..3915e16b893d 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -22,7 +22,6 @@ import {FormArrayName} from './reactive_directives/form_group_name'; import {ngModelWarning} from './reactive_errors'; import {AsyncValidatorFn, Validator, ValidatorFn} from './validators'; - export function controlPath(name: string|null, parent: ControlContainer): string[] { return [...parent.path!, name!]; } @@ -91,7 +90,7 @@ export function cleanUpControl( } function registerOnValidatorChange(validators: (V|Validator)[], onChange: () => void): void { - validators.forEach((validator: (V|Validator)) => { + validators.forEach((validator: V|Validator) => { if ((validator).registerOnValidatorChange) (validator).registerOnValidatorChange!(onChange); }); @@ -172,7 +171,7 @@ export function cleanUpValidators( const validators = getControlValidators(control); if (Array.isArray(validators) && validators.length > 0) { // Filter out directive validator function. - const updatedValidators = validators.filter(validator => validator !== dir.validator); + const updatedValidators = validators.filter((validator) => validator !== dir.validator); if (updatedValidators.length !== validators.length) { isControlUpdated = true; control.setValidators(updatedValidators); @@ -185,7 +184,7 @@ export function cleanUpValidators( if (Array.isArray(asyncValidators) && asyncValidators.length > 0) { // Filter out directive async validator function. const updatedAsyncValidators = - asyncValidators.filter(asyncValidator => asyncValidator !== dir.asyncValidator); + asyncValidators.filter((asyncValidator) => asyncValidator !== dir.asyncValidator); if (updatedAsyncValidators.length !== asyncValidators.length) { isControlUpdated = true; control.setAsyncValidators(updatedAsyncValidators); @@ -275,18 +274,23 @@ function _noControlError(dir: NgControl) { return _throwError(dir, 'There is no FormControl instance attached to form control element with'); } -function _throwError(dir: AbstractControlDirective, message: string, extraMessage?: string): void { - let messageEnd: string; - if (dir.path!.length > 1) { - messageEnd = `path: '${dir.path!.join(' -> ')}'`; - } else if (dir.path![0]) { - messageEnd = `name: '${dir.path}'`; - } else { - messageEnd = 'unspecified name attribute.'; - } +function _throwError(dir: AbstractControlDirective, message: string): void { + const messageEnd = _describeControlLocation(dir); + throw new Error(`${message} ${messageEnd}`); +} - extraMessage = extraMessage ? '\n' + extraMessage : ''; - throw new Error(`${message} ${messageEnd}${extraMessage}`); +function _describeControlLocation(dir: AbstractControlDirective): string { + const path = dir.path; + if (path && path.length > 1) return `path: '${path.join(' -> ')}'`; + if (path?.[0]) return `name: '${path}'`; + return 'unspecified name attribute'; +} + +function _throwInvalidValueAccessorError(dir: AbstractControlDirective) { + const loc = _describeControlLocation(dir); + throw new Error( + `Value accessor was not provided as an array for form control with ${loc}. ` + + `Check that the \`NG_VALUE_ACCESSOR\` token is configured as a \`multi: true\` provider.`); } export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any): boolean { @@ -320,9 +324,7 @@ export function selectValueAccessor( if (!valueAccessors) return null; if (!Array.isArray(valueAccessors) && (typeof ngDevMode === 'undefined' || ngDevMode)) - _throwError( - dir, 'Value accessor was not provided as an array for form control with', - 'Please make sure that the `NG_VALUE_ACCESSOR` token is configured as a multi provider (with the `multi: true` property).'); + _throwInvalidValueAccessorError(dir); let defaultAccessor: ControlValueAccessor|undefined = undefined; let builtinAccessor: ControlValueAccessor|undefined = undefined; @@ -331,12 +333,10 @@ export function selectValueAccessor( valueAccessors.forEach((v: ControlValueAccessor) => { if (v.constructor === DefaultValueAccessor) { defaultAccessor = v; - } else if (isBuiltInAccessor(v)) { if (builtinAccessor && (typeof ngDevMode === 'undefined' || ngDevMode)) _throwError(dir, 'More than one built-in value accessor matches form control with'); builtinAccessor = v; - } else { if (customAccessor && (typeof ngDevMode === 'undefined' || ngDevMode)) _throwError(dir, 'More than one custom value accessor matches form control with'); diff --git a/packages/forms/test/directives_spec.ts b/packages/forms/test/directives_spec.ts index 28cb2c704eee..90d138de139c 100644 --- a/packages/forms/test/directives_spec.ts +++ b/packages/forms/test/directives_spec.ts @@ -54,7 +54,7 @@ class CustomValidatorDirective implements Validator { it('should throw when accessor is not provided as array', () => { expect(() => selectValueAccessor(dir, {} as any[])) .toThrowError( - 'Value accessor was not provided as an array for form control with unspecified name attribute.\nPlease make sure that the `NG_VALUE_ACCESSOR` token is configured as a multi provider (with the `multi: true` property).'); + 'Value accessor was not provided as an array for form control with unspecified name attribute. Check that the \`NG_VALUE_ACCESSOR\` token is configured as a \`multi: true\` provider.'); }); it('should return the default value accessor when no other provided', () => {