Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,6 @@
"hasInSkipHydrationBlockFlag",
"hasParentInjector",
"hasTagAndTypeMatch",
"hasValidLength",
"hasValidator",
"icuContainerIterate",
"identity",
Expand Down Expand Up @@ -493,6 +492,7 @@
"leaveDI",
"leaveView",
"leaveViewLight",
"lengthOrSize",
"lookupTokenUsingModuleInjector",
"lookupTokenUsingNodeInjector",
"makeParamDecorator",
Expand Down
80 changes: 49 additions & 31 deletions packages/forms/src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
};
}

Expand Down
50 changes: 50 additions & 0 deletions packages/forms/test/validators_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
Expand Down