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:
parent
eefe1682e8
commit
a502279592
|
@ -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)
|
||||
|
||||
```
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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` */;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
Loading…
Reference in New Issue