feat(forms): allow minLength/maxLength validator to be bound to `null` (#42565)

If the validator is bound to be `null` then no validation occurs and
attribute is not added to DOM.

For every validator type different PR will be raised as discussed in
https://github.com/angular/angular/pull/42378.

Closes #42267.

PR Close #42565
This commit is contained in:
iRealNirmal 2021-06-11 19:32:47 +05:30 committed by Dylan Hunn
parent eefe1682e8
commit a502279592
6 changed files with 231 additions and 24 deletions

View File

@ -384,7 +384,7 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan
resetForm(value?: any): void;
readonly submitted: boolean;
updateModel(dir: FormControlName, value: any): void;
}
}
// @public
export class FormGroupName extends AbstractFormGroupDirective implements OnInit, OnDestroy {
@ -398,12 +398,14 @@ export class FormsModule {
// @public
export class MaxLengthValidator implements Validator, OnChanges {
maxlength: string | number;
// (undocumented)
enabled(): boolean;
maxlength: string | number | null;
// (undocumented)
ngOnChanges(changes: SimpleChanges): void;
registerOnValidatorChange(fn: () => void): void;
validate(control: AbstractControl): ValidationErrors | null;
}
}
// @public
export class MaxValidator extends AbstractValidatorDirective implements OnChanges {
@ -413,12 +415,14 @@ export class MaxValidator extends AbstractValidatorDirective implements OnChange
// @public
export class MinLengthValidator implements Validator, OnChanges {
minlength: string | number;
// (undocumented)
enabled(): boolean;
minlength: string | number | null;
// (undocumented)
ngOnChanges(changes: SimpleChanges): void;
registerOnValidatorChange(fn: () => void): void;
validate(control: AbstractControl): ValidationErrors | null;
}
}
// @public
export class MinValidator extends AbstractValidatorDirective implements OnChanges {
@ -539,7 +543,7 @@ export class PatternValidator implements Validator, OnChanges {
pattern: string | RegExp;
registerOnValidatorChange(fn: () => void): void;
validate(control: AbstractControl): ValidationErrors | null;
}
}
// @public
export class RadioControlValueAccessor extends ɵangular_packages_forms_forms_g implements ControlValueAccessor, OnDestroy, OnInit {
@ -632,7 +636,6 @@ export class Validators {
// @public (undocumented)
export const VERSION: Version;
// (No @packageDocumentation comment for this package)
```

View File

@ -23,7 +23,8 @@ describe('ngModelGroup example', () => {
it('should populate the UI with initial values', () => {
expect(inputs.get(0).getAttribute('value')).toEqual('Nancy');
expect(inputs.get(1).getAttribute('value')).toEqual('Drew');
expect(inputs.get(1).getAttribute('value')).toEqual('J');
expect(inputs.get(2).getAttribute('value')).toEqual('Drew');
});
it('should show the error when name is invalid', () => {
@ -37,6 +38,7 @@ describe('ngModelGroup example', () => {
it('should set the value when changing the domain model', () => {
buttons.get(1).click();
expect(inputs.get(0).getAttribute('value')).toEqual('Bess');
expect(inputs.get(1).getAttribute('value')).toEqual('Marvin');
expect(inputs.get(1).getAttribute('value')).toEqual('S');
expect(inputs.get(2).getAttribute('value')).toEqual('Marvin');
});
});

View File

@ -19,6 +19,7 @@ import {NgForm} from '@angular/forms';
<div ngModelGroup="name" #nameCtrl="ngModelGroup">
<input name="first" [ngModel]="name.first" minlength="2">
<input name="middle" [ngModel]="name.middle" maxlength="2">
<input name="last" [ngModel]="name.last" required>
</div>
@ -30,15 +31,15 @@ import {NgForm} from '@angular/forms';
`,
})
export class NgModelGroupComp {
name = {first: 'Nancy', last: 'Drew'};
name = {first: 'Nancy', middle: 'J', last: 'Drew'};
onSubmit(f: NgForm) {
console.log(f.value); // {name: {first: 'Nancy', last: 'Drew'}, email: ''}
console.log(f.value); // {name: {first: 'Nancy', middle: 'J', last: 'Drew'}, email: ''}
console.log(f.valid); // true
}
setValue() {
this.name = {first: 'Bess', last: 'Marvin'};
this.name = {first: 'Bess', middle: 'S', last: 'Marvin'};
}
}
// #enddocregion

View File

@ -12,6 +12,16 @@ import {Observable} from 'rxjs';
import {AbstractControl} from '../model';
import {emailValidator, maxLengthValidator, maxValidator, minLengthValidator, minValidator, NG_VALIDATORS, nullValidator, patternValidator, requiredTrueValidator, requiredValidator} from '../validators';
/**
* @description
* Method that updates string to integer if not alread a number
*
* @param value The value to convert to integer
* @returns value of parameter in number or integer.
*/
function toNumber(value: string|number): number {
return typeof value === 'number' ? value : parseInt(value, 10);
}
/**
* @description
@ -540,7 +550,7 @@ export const MIN_LENGTH_VALIDATOR: any = {
@Directive({
selector: '[minlength][formControlName],[minlength][formControl],[minlength][ngModel]',
providers: [MIN_LENGTH_VALIDATOR],
host: {'[attr.minlength]': 'minlength ? minlength : null'}
host: {'[attr.minlength]': 'enabled() ? minlength : null'}
})
export class MinLengthValidator implements Validator, OnChanges {
private _validator: ValidatorFn = nullValidator;
@ -551,7 +561,7 @@ export class MinLengthValidator implements Validator, OnChanges {
* Tracks changes to the minimum length bound to this directive.
*/
@Input()
minlength!: string|number; // This input is always defined, since the name matches selector.
minlength!: string|number|null; // This input is always defined, since the name matches selector.
/** @nodoc */
ngOnChanges(changes: SimpleChanges): void {
@ -567,7 +577,7 @@ export class MinLengthValidator implements Validator, OnChanges {
* @nodoc
*/
validate(control: AbstractControl): ValidationErrors|null {
return this.minlength == null ? null : this._validator(control);
return this.enabled() ? this._validator(control) : null;
}
/**
@ -579,8 +589,13 @@ export class MinLengthValidator implements Validator, OnChanges {
}
private _createValidator(): void {
this._validator = minLengthValidator(
typeof this.minlength === 'number' ? this.minlength : parseInt(this.minlength, 10));
this._validator =
this.enabled() ? minLengthValidator(toNumber(this.minlength!)) : nullValidator;
}
/** @nodoc */
enabled(): boolean {
return this.minlength != null /* both `null` and `undefined` */;
}
}
@ -618,7 +633,7 @@ export const MAX_LENGTH_VALIDATOR: any = {
@Directive({
selector: '[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]',
providers: [MAX_LENGTH_VALIDATOR],
host: {'[attr.maxlength]': 'maxlength ? maxlength : null'}
host: {'[attr.maxlength]': 'enabled() ? maxlength : null'}
})
export class MaxLengthValidator implements Validator, OnChanges {
private _validator: ValidatorFn = nullValidator;
@ -629,7 +644,7 @@ export class MaxLengthValidator implements Validator, OnChanges {
* Tracks changes to the maximum length bound to this directive.
*/
@Input()
maxlength!: string|number; // This input is always defined, since the name matches selector.
maxlength!: string|number|null; // This input is always defined, since the name matches selector.
/** @nodoc */
ngOnChanges(changes: SimpleChanges): void {
@ -644,7 +659,7 @@ export class MaxLengthValidator implements Validator, OnChanges {
* @nodoc
*/
validate(control: AbstractControl): ValidationErrors|null {
return this.maxlength != null ? this._validator(control) : null;
return this.enabled() ? this._validator(control) : null;
}
/**
@ -656,8 +671,13 @@ export class MaxLengthValidator implements Validator, OnChanges {
}
private _createValidator(): void {
this._validator = maxLengthValidator(
typeof this.maxlength === 'number' ? this.maxlength : parseInt(this.maxlength, 10));
this._validator =
this.enabled() ? maxLengthValidator(toNumber(this.maxlength!)) : nullValidator;
}
/** @nodoc */
enabled(): boolean {
return this.maxlength != null /* both `null` and `undefined` */;
}
}

View File

@ -9,7 +9,7 @@
import {ɵgetDOM as getDOM} from '@angular/common';
import {Component, Directive, forwardRef, Input, NgModule, OnDestroy, Type} from '@angular/core';
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, ControlValueAccessor, DefaultValueAccessor, FormArray, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormsModule, MaxValidator, MinValidator, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validator, Validators} from '@angular/forms';
import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, ControlValueAccessor, DefaultValueAccessor, FormArray, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormsModule, MaxValidator, MinLengthValidator, MinValidator, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validator, Validators} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {dispatchEvent, sortedClassList} from '@angular/platform-browser/testing/src/browser_util';
import {merge, NEVER, of, Subscription, timer} from 'rxjs';
@ -2664,6 +2664,97 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
.toEqual(2, `Expected original observable to be canceled on the next value change.`);
}));
describe('enabling validators conditionally', () => {
it('should not activate minlength and maxlength validators if input is null', () => {
@Component({
selector: 'min-max-length-null',
template: `
<form [formGroup]="form">
<input [formControl]="control" name="control" [minlength]="minlen" [maxlength]="maxlen">
</form> `
})
class MinMaxLengthComponent {
control: FormControl = new FormControl();
form: FormGroup = new FormGroup({'control': this.control});
minlen: number|null = null;
maxlen: number|null = null;
}
const fixture = initTest(MinMaxLengthComponent);
const control = fixture.componentInstance.control;
fixture.detectChanges();
const form = fixture.componentInstance.form;
const input = fixture.debugElement.query(By.css('input')).nativeElement;
interface minmax {
minlength: number|null;
maxlength: number|null;
}
interface state {
isValid: boolean;
failedValidator?: string;
}
const setInputValue = (value: number) => {
input.value = value;
dispatchEvent(input, 'input');
fixture.detectChanges();
};
const setValidatorValues = (values: minmax) => {
fixture.componentInstance.minlen = values.minlength;
fixture.componentInstance.maxlen = values.maxlength;
fixture.detectChanges();
};
const verifyValidatorAttrValues = (values: {minlength: any, maxlength: any}) => {
expect(input.getAttribute('minlength')).toBe(values.minlength);
expect(input.getAttribute('maxlength')).toBe(values.maxlength);
};
const verifyFormState = (state: state) => {
expect(form.valid).toBe(state.isValid);
if (state.failedValidator) {
expect(control!.hasError('minlength')).toEqual(state.failedValidator === 'minlength');
expect(control!.hasError('maxlength')).toEqual(state.failedValidator === 'maxlength');
}
};
////////// Actual test scenarios start below //////////
// 1. Verify that validators are disabled when input is `null`.
setValidatorValues({minlength: null, maxlength: null});
verifyValidatorAttrValues({minlength: null, maxlength: null});
verifyFormState({isValid: true});
// 2. Verify that setting validator inputs (to a value different from `null`) activate
// validators.
setInputValue(12345);
setValidatorValues({minlength: 2, maxlength: 4});
verifyValidatorAttrValues({minlength: '2', maxlength: '4'});
verifyFormState({isValid: false, failedValidator: 'maxlength'});
// 3. Changing value to the valid range should make the form valid.
setInputValue(123);
verifyFormState({isValid: true});
// 4. Changing value to trigger `minlength` validator.
setInputValue(1);
verifyFormState({isValid: false, failedValidator: 'minlength'});
// 5. Changing validator inputs to verify that attribute values are updated (and the form
// is now valid).
setInputValue(1);
setValidatorValues({minlength: 1, maxlength: 5});
verifyValidatorAttrValues({minlength: '1', maxlength: '5'});
verifyFormState({isValid: true});
// 6. Reset validator inputs back to `null` should deactivate validators.
setInputValue(123);
setValidatorValues({minlength: null, maxlength: null});
verifyValidatorAttrValues({minlength: null, maxlength: null});
verifyFormState({isValid: true});
});
});
describe('min and max validators', () => {
function getComponent(dir: string): Type<MinMaxFormControlComp|MinMaxFormControlNameComp> {
return dir === 'formControl' ? MinMaxFormControlComp : MinMaxFormControlNameComp;

View File

@ -9,7 +9,7 @@
import {ɵgetDOM as getDOM} from '@angular/common';
import {Component, Directive, forwardRef, Input, Type, ViewChild} from '@angular/core';
import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing';
import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, ControlValueAccessor, FormControl, FormsModule, MaxValidator, MinValidator, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, NgModel, Validator} from '@angular/forms';
import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, ControlValueAccessor, FormControl, FormsModule, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, NgModel, Validator} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {dispatchEvent, sortedClassList} from '@angular/platform-browser/testing/src/browser_util';
import {merge} from 'rxjs';
@ -1917,6 +1917,96 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
expect(minValidateFnSpy).not.toHaveBeenCalled();
}));
describe('enabling validators conditionally', () => {
it('should not include the minLength and maxLength validators for null', fakeAsync(() => {
@Component({
template:
'<form><input name="amount" ngModel [minlength]="minlen" [maxlength]="maxlen"></form>'
})
class MinLengthMaxLengthComponent {
minlen: number|null = null;
maxlen: number|null = null;
control!: FormControl;
}
const fixture = initTest(MinLengthMaxLengthComponent);
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
const form = fixture.debugElement.children[0].injector.get(NgForm);
const control =
fixture.debugElement.children[0].injector.get(NgForm).control.get('amount')!;
interface minmax {
minlength: number|null;
maxlength: number|null;
}
interface state {
isValid: boolean;
failedValidator?: string;
}
const setInputValue = (value: number) => {
input.value = value;
dispatchEvent(input, 'input');
fixture.detectChanges();
};
const verifyValidatorAttrValues = (values: {minlength: any, maxlength: any}) => {
expect(input.getAttribute('minlength')).toBe(values.minlength);
expect(input.getAttribute('maxlength')).toBe(values.maxlength);
};
const setValidatorValues = (values: minmax) => {
fixture.componentInstance.minlen = values.minlength;
fixture.componentInstance.maxlen = values.maxlength;
fixture.detectChanges();
};
const verifyFormState = (state: state) => {
expect(form.valid).toBe(state.isValid);
if (state.failedValidator) {
expect(control!.hasError('minlength'))
.toEqual(state.failedValidator === 'minlength');
expect(control!.hasError('maxlength'))
.toEqual(state.failedValidator === 'maxlength');
}
};
////////// Actual test scenarios start below //////////
// 1. Verify that validators are disabled when input is `null`.
verifyValidatorAttrValues({minlength: null, maxlength: null});
verifyValidatorAttrValues({minlength: null, maxlength: null});
// 2. Verify that setting validator inputs (to a value different from `null`) activate
// validators.
setInputValue(12345);
setValidatorValues({minlength: 2, maxlength: 4});
verifyValidatorAttrValues({minlength: '2', maxlength: '4'});
verifyFormState({isValid: false, failedValidator: 'maxlength'});
// 3. Changing value to the valid range should make the form valid.
setInputValue(123);
verifyFormState({isValid: true});
// 4. Changing value to trigger `minlength` validator.
setInputValue(1);
verifyFormState({isValid: false, failedValidator: 'minlength'});
// 5. Changing validator inputs to verify that attribute values are updated (and the
// form is now valid).
setInputValue(1);
setValidatorValues({minlength: 1, maxlength: 5});
verifyValidatorAttrValues({minlength: '1', maxlength: '5'});
verifyFormState({isValid: true});
// 6. Reset validator inputs back to `null` should deactivate validators.
setInputValue(123);
setValidatorValues({minlength: null, maxlength: null});
verifyValidatorAttrValues({minlength: null, maxlength: null});
verifyFormState({isValid: true});
}));
});
['number', 'string'].forEach((inputType: string) => {
it(`should validate min and max when constraints are represented using a ${inputType}`,
fakeAsync(() => {