Skip to content

Commit fa0c3e3

Browse files
jeripeierSBBalxhub
authored andcommitted
feat(forms): support type set in form validators (#45793)
Previously, using `Validators.required`, `Validators.minLength` and `Validators.maxLength` validators don't work with sets because a set has the `size` property instead of the `length` property. This change enables the validators to be working with sets. PR Close #45793
1 parent 41b1a7c commit fa0c3e3

File tree

3 files changed

+100
-32
lines changed

3 files changed

+100
-32
lines changed

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,6 @@
418418
"hasInSkipHydrationBlockFlag",
419419
"hasParentInjector",
420420
"hasTagAndTypeMatch",
421-
"hasValidLength",
422421
"hasValidator",
423422
"icuContainerIterate",
424423
"identity",
@@ -496,6 +495,7 @@
496495
"leaveDI",
497496
"leaveView",
498497
"leaveViewLight",
498+
"lengthOrSize",
499499
"lookupTokenUsingModuleInjector",
500500
"lookupTokenUsingNodeInjector",
501501
"makeParamDecorator",

packages/forms/src/validators.ts

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,27 @@ import type {
2525
import {RuntimeErrorCode} from './errors';
2626
import type {AbstractControl} from './model/abstract_model';
2727

28-
function isEmptyInputValue(value: any): boolean {
29-
/**
30-
* Check if the object is a string or array before evaluating the length attribute.
31-
* This avoids falsely rejecting objects that contain a custom length attribute.
32-
* For example, the object {id: 1, length: 0, width: 0} should not be returned as empty.
33-
*/
34-
return (
35-
value == null || ((typeof value === 'string' || Array.isArray(value)) && value.length === 0)
36-
);
28+
function isEmptyInputValue(value: unknown): boolean {
29+
return value == null || lengthOrSize(value) === 0;
3730
}
3831

39-
function hasValidLength(value: any): boolean {
32+
/**
33+
* Extract the length property in case it's an array or a string.
34+
* Extract the size property in case it's a set.
35+
* Return null else.
36+
* @param value Either an array, set or undefined.
37+
*/
38+
function lengthOrSize(value: unknown): number | null {
4039
// non-strict comparison is intentional, to check for both `null` and `undefined` values
41-
return value != null && typeof value.length === 'number';
40+
if (value == null) {
41+
return null;
42+
} else if (Array.isArray(value) || typeof value === 'string') {
43+
return value.length;
44+
} else if (value instanceof Set) {
45+
return value.size;
46+
}
47+
48+
return null;
4249
}
4350

4451
/**
@@ -290,13 +297,14 @@ export class Validators {
290297

291298
/**
292299
* @description
293-
* Validator that requires the length of the control's value to be greater than or equal
294-
* to the provided minimum length. This validator is also provided by default if you use the
300+
* Validator that requires the number of items in the control's value to be greater than or equal
301+
* to the provided minimum length. This validator is also provided by default if you use
295302
* the HTML5 `minlength` attribute. Note that the `minLength` validator is intended to be used
296-
* only for types that have a numeric `length` property, such as strings or arrays. The
297-
* `minLength` validator logic is also not invoked for values when their `length` property is 0
298-
* (for example in case of an empty string or an empty array), to support optional controls. You
299-
* can use the standard `required` validator if empty values should not be considered valid.
303+
* only for types that have a numeric `length` or `size` property, such as strings, arrays or
304+
* sets. The `minLength` validator logic is also not invoked for values when their `length` or
305+
* `size` property is 0 (for example in case of an empty string or an empty array), to support
306+
* optional controls. You can use the standard `required` validator if empty values should not be
307+
* considered valid.
300308
*
301309
* @usageNotes
302310
*
@@ -324,10 +332,11 @@ export class Validators {
324332

325333
/**
326334
* @description
327-
* Validator that requires the length of the control's value to be less than or equal
328-
* to the provided maximum length. This validator is also provided by default if you use the
335+
* Validator that requires the number of items in the control's value to be less than or equal
336+
* to the provided maximum length. This validator is also provided by default if you use
329337
* the HTML5 `maxlength` attribute. Note that the `maxLength` validator is intended to be used
330-
* only for types that have a numeric `length` property, such as strings or arrays.
338+
* only for types that have a numeric `length` or `size` property, such as strings, arrays or
339+
* sets.
331340
*
332341
* @usageNotes
333342
*
@@ -456,7 +465,7 @@ export class Validators {
456465
*/
457466
export function minValidator(min: number): ValidatorFn {
458467
return (control: AbstractControl): ValidationErrors | null => {
459-
if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) {
468+
if (control.value == null || min == null) {
460469
return null; // don't validate empty values to allow optional controls
461470
}
462471
const value = parseFloat(control.value);
@@ -472,7 +481,7 @@ export function minValidator(min: number): ValidatorFn {
472481
*/
473482
export function maxValidator(max: number): ValidatorFn {
474483
return (control: AbstractControl): ValidationErrors | null => {
475-
if (isEmptyInputValue(control.value) || isEmptyInputValue(max)) {
484+
if (control.value == null || max == null) {
476485
return null; // don't validate empty values to allow optional controls
477486
}
478487
const value = parseFloat(control.value);
@@ -511,32 +520,41 @@ export function emailValidator(control: AbstractControl): ValidationErrors | nul
511520
}
512521

513522
/**
514-
* Validator that requires the length of the control's value to be greater than or equal
523+
* Validator that requires the number of items in the control's value to be greater than or equal
515524
* to the provided minimum length. See `Validators.minLength` for additional information.
525+
*
526+
* The minLengthValidator respects every length property in an object, regardless of whether it's an array.
527+
* For example, the object {id: 1, length: 0, width: 0} should be validated.
516528
*/
517529
export function minLengthValidator(minLength: number): ValidatorFn {
518530
return (control: AbstractControl): ValidationErrors | null => {
519-
if (isEmptyInputValue(control.value) || !hasValidLength(control.value)) {
531+
const length = control.value?.length ?? lengthOrSize(control.value);
532+
if (length === null || length === 0) {
520533
// don't validate empty values to allow optional controls
521-
// don't validate values without `length` property
534+
// don't validate values without `length` or `size` property
522535
return null;
523536
}
524537

525-
return control.value.length < minLength
526-
? {'minlength': {'requiredLength': minLength, 'actualLength': control.value.length}}
538+
return length < minLength
539+
? {'minlength': {'requiredLength': minLength, 'actualLength': length}}
527540
: null;
528541
};
529542
}
530543

531544
/**
532-
* Validator that requires the length of the control's value to be less than or equal
545+
* Validator that requires the number of items in the control's value to be less than or equal
533546
* to the provided maximum length. See `Validators.maxLength` for additional information.
547+
*
548+
* The maxLengthValidator respects every length property in an object, regardless of whether it's an array.
549+
* For example, the object {id: 1, length: 0, width: 0} should be validated.
534550
*/
535551
export function maxLengthValidator(maxLength: number): ValidatorFn {
536552
return (control: AbstractControl): ValidationErrors | null => {
537-
return hasValidLength(control.value) && control.value.length > maxLength
538-
? {'maxlength': {'requiredLength': maxLength, 'actualLength': control.value.length}}
539-
: null;
553+
const length = control.value?.length ?? lengthOrSize(control.value);
554+
if (length !== null && length > maxLength) {
555+
return {'maxlength': {'requiredLength': maxLength, 'actualLength': length}};
556+
}
557+
return null;
540558
};
541559
}
542560

packages/forms/test/validators_spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,18 @@ import {normalizeValidators} from '../src/validators';
209209
it('should not error on an object containing a length attribute that is zero', () => {
210210
expect(Validators.required(new FormControl({id: 1, length: 0, width: 0}))).toBeNull();
211211
});
212+
213+
it('should error on an empty set', () => {
214+
expect(Validators.required(new FormControl(new Set()))).toEqual({'required': true});
215+
});
216+
217+
it('should not error on a non-empty set', () => {
218+
expect(Validators.required(new FormControl(new Set([1, 2])))).toBeNull();
219+
});
220+
221+
it('should not error on an object containing a size attribute that is zero', () => {
222+
expect(Validators.required(new FormControl({id: 1, size: 0, width: 0}))).toBeNull();
223+
});
212224
});
213225

214226
describe('requiredTrue', () => {
@@ -246,6 +258,10 @@ import {normalizeValidators} from '../src/validators';
246258
expect(Validators.minLength(2)(new FormControl(undefined))).toBeNull();
247259
});
248260

261+
it('should not error on empty array', () => {
262+
expect(Validators.minLength(2)(new FormControl([]))).toBeNull();
263+
});
264+
249265
it('should not error on valid strings', () => {
250266
expect(Validators.minLength(2)(new FormControl('aa'))).toBeNull();
251267
});
@@ -287,6 +303,24 @@ import {normalizeValidators} from '../src/validators';
287303
expect(Validators.minLength(1)(new FormControl(true))).toBeNull();
288304
expect(Validators.minLength(1)(new FormControl(false))).toBeNull();
289305
});
306+
307+
it('should trigger validation for an object that contains numeric size property', () => {
308+
const value = new Set([1, 2, 3, 4, 5]);
309+
expect(Validators.minLength(1)(new FormControl(value))).toBeNull();
310+
expect(Validators.minLength(10)(new FormControl(value))).toEqual({
311+
'minlength': {'requiredLength': 10, 'actualLength': 5},
312+
});
313+
});
314+
315+
it('should not error on empty set', () => {
316+
const value = new Set();
317+
expect(Validators.minLength(1)(new FormControl(value))).toBeNull();
318+
});
319+
320+
it('should return null when passing a boolean', () => {
321+
expect(Validators.minLength(1)(new FormControl(true))).toBeNull();
322+
expect(Validators.minLength(1)(new FormControl(false))).toBeNull();
323+
});
290324
});
291325

292326
describe('maxLength', () => {
@@ -339,6 +373,22 @@ import {normalizeValidators} from '../src/validators';
339373
});
340374
});
341375

376+
it('should trigger validation for an object that contains numeric length property', () => {
377+
const value = {length: 5, someValue: [1, 2, 3, 4, 5]};
378+
expect(Validators.maxLength(10)(new FormControl(value))).toBeNull();
379+
expect(Validators.maxLength(1)(new FormControl(value))).toEqual({
380+
'maxlength': {'requiredLength': 1, 'actualLength': 5},
381+
});
382+
});
383+
384+
it('should trigger validation for an object that contains numeric size property', () => {
385+
const value = new Set([1, 2, 3, 4, 5]);
386+
expect(Validators.maxLength(10)(new FormControl(value))).toBeNull();
387+
expect(Validators.maxLength(1)(new FormControl(value))).toEqual({
388+
'maxlength': {'requiredLength': 1, 'actualLength': 5},
389+
});
390+
});
391+
342392
it('should return null when passing a boolean', () => {
343393
expect(Validators.maxLength(1)(new FormControl(true))).toBeNull();
344394
expect(Validators.maxLength(1)(new FormControl(false))).toBeNull();

0 commit comments

Comments
 (0)