Skip to content

Commit d9d8f95

Browse files
iRealNirmalalxhub
authored andcommitted
feat(forms): allow disabling min/max validators dynamically (by setting the value to null) (#42978)
This commit updates the logic of the `min` and `max` validators to allow disabling them dynamically in case `null` is provided as a value. For example: `<input type="number" [min]="minValue">`, when `minValue` might be set to `null` in a component class. This should allow `min` and `max` validators to be used for dynamic forms. Note: similar support was added to the `minLength` and `maxLength` validators earlier (see #42565). PR Close #42978
1 parent fe69193 commit d9d8f95

File tree

4 files changed

+208
-13
lines changed

4 files changed

+208
-13
lines changed

goldens/public-api/forms/forms.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ export class MaxLengthValidator implements Validator, OnChanges {
415415

416416
// @public
417417
export class MaxValidator extends AbstractValidatorDirective implements OnChanges {
418-
max: string | number;
418+
max: string | number | null;
419419
ngOnChanges(changes: SimpleChanges): void;
420420
}
421421

@@ -432,7 +432,7 @@ export class MinLengthValidator implements Validator, OnChanges {
432432

433433
// @public
434434
export class MinValidator extends AbstractValidatorDirective implements OnChanges {
435-
min: string | number;
435+
min: string | number | null;
436436
ngOnChanges(changes: SimpleChanges): void;
437437
}
438438

packages/forms/src/directives/validators.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,24 @@ import {AbstractControl} from '../model';
1313
import {emailValidator, maxLengthValidator, maxValidator, minLengthValidator, minValidator, NG_VALIDATORS, nullValidator, patternValidator, requiredTrueValidator, requiredValidator} from '../validators';
1414

1515
/**
16-
* @description
1716
* Method that updates string to integer if not alread a number
1817
*
1918
* @param value The value to convert to integer
2019
* @returns value of parameter in number or integer.
2120
*/
22-
function toNumber(value: string|number): number {
21+
function toInteger(value: string|number): number {
2322
return typeof value === 'number' ? value : parseInt(value, 10);
2423
}
2524

25+
/**
26+
* Method that ensures that provided value is a float (and converts it to float if needed).
27+
*
28+
* @param value The value to convert to float
29+
* @returns value of parameter in number or float.
30+
*/
31+
function toFloat(value: string|number): number {
32+
return typeof value === 'number' ? value : parseFloat(value);
33+
}
2634
/**
2735
* @description
2836
* Defines the map of errors returned from failed validation checks.
@@ -124,7 +132,7 @@ abstract class AbstractValidatorDirective implements Validator {
124132
handleChanges(changes: SimpleChanges): void {
125133
if (this.inputName in changes) {
126134
const input = this.normalizeInput(changes[this.inputName].currentValue);
127-
this._validator = this.createValidator(input);
135+
this._validator = this.enabled() ? this.createValidator(input) : nullValidator;
128136
if (this._onChange) {
129137
this._onChange();
130138
}
@@ -140,6 +148,18 @@ abstract class AbstractValidatorDirective implements Validator {
140148
registerOnValidatorChange(fn: () => void): void {
141149
this._onChange = fn;
142150
}
151+
152+
/**
153+
* @description
154+
* Determines whether this validator is active or not. Base class implementation
155+
* checks whether an input is defined (if the value is different from `null` and `undefined`).
156+
* Validator classes that extend this base class can override this function with the logic
157+
* specific to a particular validator directive.
158+
*/
159+
enabled(): boolean {
160+
const inputValue = (this as unknown as {[key: string]: unknown})[this.inputName];
161+
return inputValue != null /* both `null` and `undefined` */;
162+
}
143163
}
144164

145165
/**
@@ -177,18 +197,18 @@ export const MAX_VALIDATOR: StaticProvider = {
177197
selector:
178198
'input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]',
179199
providers: [MAX_VALIDATOR],
180-
host: {'[attr.max]': 'max ?? null'}
200+
host: {'[attr.max]': 'enabled() ? max : null'}
181201
})
182202
export class MaxValidator extends AbstractValidatorDirective implements OnChanges {
183203
/**
184204
* @description
185205
* Tracks changes to the max bound to this directive.
186206
*/
187-
@Input() max!: string|number;
207+
@Input() max!: string|number|null;
188208
/** @internal */
189209
override inputName = 'max';
190210
/** @internal */
191-
override normalizeInput = (input: string): number => parseFloat(input);
211+
override normalizeInput = (input: string|number): number => toFloat(input);
192212
/** @internal */
193213
override createValidator = (max: number): ValidatorFn => maxValidator(max);
194214
/**
@@ -237,18 +257,18 @@ export const MIN_VALIDATOR: StaticProvider = {
237257
selector:
238258
'input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]',
239259
providers: [MIN_VALIDATOR],
240-
host: {'[attr.min]': 'min ?? null'}
260+
host: {'[attr.min]': 'enabled() ? min : null'}
241261
})
242262
export class MinValidator extends AbstractValidatorDirective implements OnChanges {
243263
/**
244264
* @description
245265
* Tracks changes to the min bound to this directive.
246266
*/
247-
@Input() min!: string|number;
267+
@Input() min!: string|number|null;
248268
/** @internal */
249269
override inputName = 'min';
250270
/** @internal */
251-
override normalizeInput = (input: string): number => parseFloat(input);
271+
override normalizeInput = (input: string|number): number => toFloat(input);
252272
/** @internal */
253273
override createValidator = (min: number): ValidatorFn => minValidator(min);
254274
/**
@@ -590,7 +610,7 @@ export class MinLengthValidator implements Validator, OnChanges {
590610

591611
private _createValidator(): void {
592612
this._validator =
593-
this.enabled() ? minLengthValidator(toNumber(this.minlength!)) : nullValidator;
613+
this.enabled() ? minLengthValidator(toInteger(this.minlength!)) : nullValidator;
594614
}
595615

596616
/** @nodoc */
@@ -672,7 +692,7 @@ export class MaxLengthValidator implements Validator, OnChanges {
672692

673693
private _createValidator(): void {
674694
this._validator =
675-
this.enabled() ? maxLengthValidator(toNumber(this.maxlength!)) : nullValidator;
695+
this.enabled() ? maxLengthValidator(toInteger(this.maxlength!)) : nullValidator;
676696
}
677697

678698
/** @nodoc */

packages/forms/test/reactive_integration_spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2753,6 +2753,95 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
27532753
verifyValidatorAttrValues({minlength: null, maxlength: null});
27542754
verifyFormState({isValid: true});
27552755
});
2756+
2757+
it('should not activate min and max validators if input is null', () => {
2758+
@Component({
2759+
selector: 'min-max-null',
2760+
template: `
2761+
<form [formGroup]="form">
2762+
<input type="number" [formControl]="control" name="minmaxinput" [min]="minlen" [max]="maxlen">
2763+
</form> `
2764+
})
2765+
class MinMaxComponent {
2766+
control: FormControl = new FormControl();
2767+
form: FormGroup = new FormGroup({'control': this.control});
2768+
minlen: number|null = null;
2769+
maxlen: number|null = null;
2770+
}
2771+
2772+
const fixture = initTest(MinMaxComponent);
2773+
const control = fixture.componentInstance.control;
2774+
fixture.detectChanges();
2775+
2776+
const form = fixture.componentInstance.form;
2777+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
2778+
2779+
interface minmax {
2780+
min: number|null;
2781+
max: number|null;
2782+
}
2783+
2784+
interface state {
2785+
isValid: boolean;
2786+
failedValidator?: string;
2787+
}
2788+
2789+
const setInputValue = (value: number) => {
2790+
input.value = value;
2791+
dispatchEvent(input, 'input');
2792+
fixture.detectChanges();
2793+
};
2794+
const setValidatorValues = (values: minmax) => {
2795+
fixture.componentInstance.minlen = values.min;
2796+
fixture.componentInstance.maxlen = values.max;
2797+
fixture.detectChanges();
2798+
};
2799+
const verifyValidatorAttrValues = (values: {min: any, max: any}) => {
2800+
expect(input.getAttribute('min')).toBe(values.min);
2801+
expect(input.getAttribute('max')).toBe(values.max);
2802+
};
2803+
const verifyFormState = (state: state) => {
2804+
expect(form.valid).toBe(state.isValid);
2805+
if (state.failedValidator) {
2806+
expect(control!.hasError('min')).toEqual(state.failedValidator === 'min');
2807+
expect(control!.hasError('max')).toEqual(state.failedValidator === 'max');
2808+
}
2809+
};
2810+
2811+
////////// Actual test scenarios start below //////////
2812+
// 1. Verify that validators are disabled when input is `null`.
2813+
setValidatorValues({min: null, max: null});
2814+
verifyValidatorAttrValues({min: null, max: null});
2815+
verifyFormState({isValid: true});
2816+
2817+
// 2. Verify that setting validator inputs (to a value different from `null`) activate
2818+
// validators.
2819+
setInputValue(12345);
2820+
setValidatorValues({min: 2, max: 4});
2821+
verifyValidatorAttrValues({min: '2', max: '4'});
2822+
verifyFormState({isValid: false, failedValidator: 'max'});
2823+
2824+
// 3. Changing value to the valid range should make the form valid.
2825+
setInputValue(3);
2826+
verifyFormState({isValid: true});
2827+
2828+
// 4. Changing value to trigger `minlength` validator.
2829+
setInputValue(1);
2830+
verifyFormState({isValid: false, failedValidator: 'min'});
2831+
2832+
// 5. Changing validator inputs to verify that attribute values are updated (and the form
2833+
// is now valid).
2834+
setInputValue(1);
2835+
setValidatorValues({min: 1, max: 5});
2836+
verifyValidatorAttrValues({min: '1', max: '5'});
2837+
verifyFormState({isValid: true});
2838+
2839+
// 6. Reset validator inputs back to `null` should deactivate validators.
2840+
setInputValue(123);
2841+
setValidatorValues({min: null, max: null});
2842+
verifyValidatorAttrValues({min: null, max: null});
2843+
verifyFormState({isValid: true});
2844+
});
27562845
});
27572846

27582847
describe('min and max validators', () => {

packages/forms/test/template_integration_spec.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,6 +2005,92 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
20052005
verifyValidatorAttrValues({minlength: null, maxlength: null});
20062006
verifyFormState({isValid: true});
20072007
}));
2008+
2009+
it('should not include the min and max validators for null', fakeAsync(() => {
2010+
@Component({
2011+
template:
2012+
'<form><input type="number" name="minmaxinput" ngModel [min]="minlen" [max]="maxlen"></form>'
2013+
})
2014+
class MinLengthMaxLengthComponent {
2015+
minlen: number|null = null;
2016+
maxlen: number|null = null;
2017+
control!: FormControl;
2018+
}
2019+
2020+
const fixture = initTest(MinLengthMaxLengthComponent);
2021+
fixture.detectChanges();
2022+
tick();
2023+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
2024+
2025+
const form = fixture.debugElement.children[0].injector.get(NgForm);
2026+
const control =
2027+
fixture.debugElement.children[0].injector.get(NgForm).control.get('minmaxinput')!;
2028+
2029+
interface minmax {
2030+
min: number|null;
2031+
max: number|null;
2032+
}
2033+
2034+
interface state {
2035+
isValid: boolean;
2036+
failedValidator?: string;
2037+
}
2038+
2039+
const setInputValue = (value: number) => {
2040+
input.value = value;
2041+
dispatchEvent(input, 'input');
2042+
fixture.detectChanges();
2043+
};
2044+
const verifyValidatorAttrValues = (values: {min: any, max: any}) => {
2045+
expect(input.getAttribute('min')).toBe(values.min);
2046+
expect(input.getAttribute('max')).toBe(values.max);
2047+
};
2048+
const setValidatorValues = (values: minmax) => {
2049+
fixture.componentInstance.minlen = values.min;
2050+
fixture.componentInstance.maxlen = values.max;
2051+
fixture.detectChanges();
2052+
};
2053+
const verifyFormState = (state: state) => {
2054+
expect(form.valid).toBe(state.isValid);
2055+
if (state.failedValidator) {
2056+
expect(control!.hasError('min')).toEqual(state.failedValidator === 'min');
2057+
expect(control!.hasError('max')).toEqual(state.failedValidator === 'max');
2058+
}
2059+
};
2060+
2061+
////////// Actual test scenarios start below //////////
2062+
// 1. Verify that validators are disabled when input is `null`.
2063+
verifyValidatorAttrValues({min: null, max: null});
2064+
verifyValidatorAttrValues({min: null, max: null});
2065+
2066+
// 2. Verify that setting validator inputs (to a value different from `null`) activate
2067+
// validators.
2068+
setInputValue(12345);
2069+
setValidatorValues({min: 2, max: 4});
2070+
verifyValidatorAttrValues({min: '2', max: '4'});
2071+
verifyFormState({isValid: false, failedValidator: 'max'});
2072+
2073+
// 3. Changing value to the valid range should make the form valid.
2074+
setInputValue(3);
2075+
verifyFormState({isValid: true});
2076+
2077+
// 4. Changing value to trigger `minlength` validator.
2078+
setInputValue(1);
2079+
verifyFormState({isValid: false, failedValidator: 'min'});
2080+
2081+
// 5. Changing validator inputs to verify that attribute values are updated (and the
2082+
// form is now valid).
2083+
setInputValue(1);
2084+
setValidatorValues({min: 1, max: 5});
2085+
verifyValidatorAttrValues({min: '1', max: '5'});
2086+
verifyFormState({isValid: true});
2087+
2088+
// 6. Reset validator inputs back to `null` should deactivate validators.
2089+
setInputValue(123);
2090+
setValidatorValues({min: null, max: null});
2091+
verifyValidatorAttrValues({min: null, max: null});
2092+
verifyFormState({isValid: true});
2093+
}));
20082094
});
20092095

20102096
['number', 'string'].forEach((inputType: string) => {

0 commit comments

Comments
 (0)