diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index 219fd3384361..d3d0fc46adc9 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -414,7 +414,6 @@ "hasInSkipHydrationBlockFlag", "hasParentInjector", "hasTagAndTypeMatch", - "hasValidLength", "hasValidator", "icuContainerIterate", "identity", @@ -493,6 +492,7 @@ "leaveDI", "leaveView", "leaveViewLight", + "lengthOrSize", "lookupTokenUsingModuleInjector", "lookupTokenUsingNodeInjector", "makeParamDecorator", diff --git a/packages/forms/src/validators.ts b/packages/forms/src/validators.ts index 02972fb71d8f..18dc22a86176 100644 --- a/packages/forms/src/validators.ts +++ b/packages/forms/src/validators.ts @@ -25,20 +25,27 @@ import type { import {RuntimeErrorCode} from './errors'; import type {AbstractControl} from './model/abstract_model'; -function isEmptyInputValue(value: any): boolean { - /** - * Check if the object is a string or array before evaluating the length attribute. - * This avoids falsely rejecting objects that contain a custom length attribute. - * For example, the object {id: 1, length: 0, width: 0} should not be returned as empty. - */ - return ( - value == null || ((typeof value === 'string' || Array.isArray(value)) && value.length === 0) - ); +function isEmptyInputValue(value: unknown): boolean { + return value == null || lengthOrSize(value) === 0; } -function hasValidLength(value: any): boolean { +/** + * Extract the length property in case it's an array or a string. + * Extract the size property in case it's a set. + * Return null else. + * @param value Either an array, set or undefined. + */ +function lengthOrSize(value: unknown): number | null { // non-strict comparison is intentional, to check for both `null` and `undefined` values - return value != null && typeof value.length === 'number'; + if (value == null) { + return null; + } else if (Array.isArray(value) || typeof value === 'string') { + return value.length; + } else if (value instanceof Set) { + return value.size; + } + + return null; } /** @@ -290,13 +297,14 @@ export class Validators { /** * @description - * Validator that requires the length of the control's value to be greater than or equal - * to the provided minimum length. This validator is also provided by default if you use the + * Validator that requires the number of items in the control's value to be greater than or equal + * to the provided minimum length. This validator is also provided by default if you use * the HTML5 `minlength` attribute. Note that the `minLength` validator is intended to be used - * only for types that have a numeric `length` property, such as strings or arrays. The - * `minLength` validator logic is also not invoked for values when their `length` property is 0 - * (for example in case of an empty string or an empty array), to support optional controls. You - * can use the standard `required` validator if empty values should not be considered valid. + * only for types that have a numeric `length` or `size` property, such as strings, arrays or + * sets. The `minLength` validator logic is also not invoked for values when their `length` or + * `size` property is 0 (for example in case of an empty string or an empty array), to support + * optional controls. You can use the standard `required` validator if empty values should not be + * considered valid. * * @usageNotes * @@ -324,10 +332,11 @@ export class Validators { /** * @description - * Validator that requires the length of the control's value to be less than or equal - * to the provided maximum length. This validator is also provided by default if you use the + * Validator that requires the number of items in the control's value to be less than or equal + * to the provided maximum length. This validator is also provided by default if you use * the HTML5 `maxlength` attribute. Note that the `maxLength` validator is intended to be used - * only for types that have a numeric `length` property, such as strings or arrays. + * only for types that have a numeric `length` or `size` property, such as strings, arrays or + * sets. * * @usageNotes * @@ -456,7 +465,7 @@ export class Validators { */ export function minValidator(min: number): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) { + if (control.value == null || min == null) { return null; // don't validate empty values to allow optional controls } const value = parseFloat(control.value); @@ -472,7 +481,7 @@ export function minValidator(min: number): ValidatorFn { */ export function maxValidator(max: number): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - if (isEmptyInputValue(control.value) || isEmptyInputValue(max)) { + if (control.value == null || max == null) { return null; // don't validate empty values to allow optional controls } const value = parseFloat(control.value); @@ -511,32 +520,41 @@ export function emailValidator(control: AbstractControl): ValidationErrors | nul } /** - * Validator that requires the length of the control's value to be greater than or equal + * Validator that requires the number of items in the control's value to be greater than or equal * to the provided minimum length. See `Validators.minLength` for additional information. + * + * The minLengthValidator respects every length property in an object, regardless of whether it's an array. + * For example, the object {id: 1, length: 0, width: 0} should be validated. */ export function minLengthValidator(minLength: number): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - if (isEmptyInputValue(control.value) || !hasValidLength(control.value)) { + const length = control.value?.length ?? lengthOrSize(control.value); + if (length === null || length === 0) { // don't validate empty values to allow optional controls - // don't validate values without `length` property + // don't validate values without `length` or `size` property return null; } - return control.value.length < minLength - ? {'minlength': {'requiredLength': minLength, 'actualLength': control.value.length}} + return length < minLength + ? {'minlength': {'requiredLength': minLength, 'actualLength': length}} : null; }; } /** - * Validator that requires the length of the control's value to be less than or equal + * Validator that requires the number of items in the control's value to be less than or equal * to the provided maximum length. See `Validators.maxLength` for additional information. + * + * The maxLengthValidator respects every length property in an object, regardless of whether it's an array. + * For example, the object {id: 1, length: 0, width: 0} should be validated. */ export function maxLengthValidator(maxLength: number): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - return hasValidLength(control.value) && control.value.length > maxLength - ? {'maxlength': {'requiredLength': maxLength, 'actualLength': control.value.length}} - : null; + const length = control.value?.length ?? lengthOrSize(control.value); + if (length !== null && length > maxLength) { + return {'maxlength': {'requiredLength': maxLength, 'actualLength': length}}; + } + return null; }; } diff --git a/packages/forms/test/validators_spec.ts b/packages/forms/test/validators_spec.ts index 0a6519c7e95f..572378dddd49 100644 --- a/packages/forms/test/validators_spec.ts +++ b/packages/forms/test/validators_spec.ts @@ -209,6 +209,18 @@ import {normalizeValidators} from '../src/validators'; it('should not error on an object containing a length attribute that is zero', () => { expect(Validators.required(new FormControl({id: 1, length: 0, width: 0}))).toBeNull(); }); + + it('should error on an empty set', () => { + expect(Validators.required(new FormControl(new Set()))).toEqual({'required': true}); + }); + + it('should not error on a non-empty set', () => { + expect(Validators.required(new FormControl(new Set([1, 2])))).toBeNull(); + }); + + it('should not error on an object containing a size attribute that is zero', () => { + expect(Validators.required(new FormControl({id: 1, size: 0, width: 0}))).toBeNull(); + }); }); describe('requiredTrue', () => { @@ -246,6 +258,10 @@ import {normalizeValidators} from '../src/validators'; expect(Validators.minLength(2)(new FormControl(undefined))).toBeNull(); }); + it('should not error on empty array', () => { + expect(Validators.minLength(2)(new FormControl([]))).toBeNull(); + }); + it('should not error on valid strings', () => { expect(Validators.minLength(2)(new FormControl('aa'))).toBeNull(); }); @@ -287,6 +303,24 @@ import {normalizeValidators} from '../src/validators'; expect(Validators.minLength(1)(new FormControl(true))).toBeNull(); expect(Validators.minLength(1)(new FormControl(false))).toBeNull(); }); + + it('should trigger validation for an object that contains numeric size property', () => { + const value = new Set([1, 2, 3, 4, 5]); + expect(Validators.minLength(1)(new FormControl(value))).toBeNull(); + expect(Validators.minLength(10)(new FormControl(value))).toEqual({ + 'minlength': {'requiredLength': 10, 'actualLength': 5}, + }); + }); + + it('should not error on empty set', () => { + const value = new Set(); + expect(Validators.minLength(1)(new FormControl(value))).toBeNull(); + }); + + it('should return null when passing a boolean', () => { + expect(Validators.minLength(1)(new FormControl(true))).toBeNull(); + expect(Validators.minLength(1)(new FormControl(false))).toBeNull(); + }); }); describe('maxLength', () => { @@ -339,6 +373,22 @@ import {normalizeValidators} from '../src/validators'; }); }); + it('should trigger validation for an object that contains numeric length property', () => { + const value = {length: 5, someValue: [1, 2, 3, 4, 5]}; + expect(Validators.maxLength(10)(new FormControl(value))).toBeNull(); + expect(Validators.maxLength(1)(new FormControl(value))).toEqual({ + 'maxlength': {'requiredLength': 1, 'actualLength': 5}, + }); + }); + + it('should trigger validation for an object that contains numeric size property', () => { + const value = new Set([1, 2, 3, 4, 5]); + expect(Validators.maxLength(10)(new FormControl(value))).toBeNull(); + expect(Validators.maxLength(1)(new FormControl(value))).toEqual({ + 'maxlength': {'requiredLength': 1, 'actualLength': 5}, + }); + }); + it('should return null when passing a boolean', () => { expect(Validators.maxLength(1)(new FormControl(true))).toBeNull(); expect(Validators.maxLength(1)(new FormControl(false))).toBeNull();