feat(forms): introduce min and max validators (#39063)
This commit adds the missing `min` and `max` validators. BREAKING CHANGE: Previously `min` and `max` attributes defined on the `<input type="number">` were ignored by Forms module. Now presence of these attributes would trigger min/max validation logic (in case `formControl`, `formControlName` or `ngModel` directives are also present on a given input) and corresponding form control status would reflect that. Fixes #16352 PR Close #39063
This commit is contained in:
parent
d067dc0cb9
commit
8fb83ea1b5
|
@ -337,6 +337,11 @@ export declare class MaxLengthValidator implements Validator, OnChanges {
|
|||
validate(control: AbstractControl): ValidationErrors | null;
|
||||
}
|
||||
|
||||
export declare class MaxValidator extends AbstractValidatorDirective implements OnChanges {
|
||||
max: string | number;
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
}
|
||||
|
||||
export declare class MinLengthValidator implements Validator, OnChanges {
|
||||
minlength: string | number;
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
|
@ -344,6 +349,11 @@ export declare class MinLengthValidator implements Validator, OnChanges {
|
|||
validate(control: AbstractControl): ValidationErrors | null;
|
||||
}
|
||||
|
||||
export declare class MinValidator extends AbstractValidatorDirective implements OnChanges {
|
||||
min: string | number;
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
}
|
||||
|
||||
export declare const NG_ASYNC_VALIDATORS: InjectionToken<(Function | Validator)[]>;
|
||||
|
||||
export declare const NG_VALIDATORS: InjectionToken<(Function | Validator)[]>;
|
||||
|
|
|
@ -24,7 +24,7 @@ import {FormGroupDirective} from './directives/reactive_directives/form_group_di
|
|||
import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
|
||||
import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
|
||||
import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
|
||||
import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
|
||||
import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator} from './directives/validators';
|
||||
|
||||
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
|
||||
export {ControlValueAccessor} from './directives/control_value_accessor';
|
||||
|
@ -63,6 +63,8 @@ export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
|
|||
PatternValidator,
|
||||
CheckboxRequiredValidator,
|
||||
EmailValidator,
|
||||
MinValidator,
|
||||
MaxValidator,
|
||||
];
|
||||
|
||||
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];
|
||||
|
|
|
@ -69,6 +69,189 @@ export interface Validator {
|
|||
registerOnValidatorChange?(fn: () => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class for Validator-based Directives. The class contains common logic shared across such
|
||||
* Directives.
|
||||
*
|
||||
* For internal use only, this class is not intended for use outside of the Forms package.
|
||||
*/
|
||||
@Directive()
|
||||
abstract class AbstractValidatorDirective implements Validator {
|
||||
private _validator: ValidatorFn = Validators.nullValidator;
|
||||
private _onChange!: () => void;
|
||||
|
||||
/**
|
||||
* Name of an input that matches directive selector attribute (e.g. `minlength` for
|
||||
* `MinLengthDirective`). An input with a given name might contain configuration information (like
|
||||
* `minlength='10'`) or a flag that indicates whether validator should be enabled (like
|
||||
* `[required]='false'`).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract inputName: string;
|
||||
|
||||
/**
|
||||
* Creates an instance of a validator (specific to a directive that extends this base class).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract createValidator(input: unknown): ValidatorFn;
|
||||
|
||||
/**
|
||||
* Performs the necessary input normalization based on a specific logic of a Directive.
|
||||
* For example, the function might be used to convert string-based representation of the
|
||||
* `minlength` input to an integer value that can later be used in the `Validators.minLength`
|
||||
* validator.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract normalizeInput(input: unknown): unknown;
|
||||
|
||||
/**
|
||||
* Helper function invoked from child classes to process changes (from `ngOnChanges` hook).
|
||||
* @nodoc
|
||||
*/
|
||||
handleChanges(changes: SimpleChanges): void {
|
||||
if (this.inputName in changes) {
|
||||
const input = this.normalizeInput(changes[this.inputName].currentValue);
|
||||
this._validator = this.createValidator(input);
|
||||
if (this._onChange) {
|
||||
this._onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
validate(control: AbstractControl): ValidationErrors|null {
|
||||
return this._validator(control);
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
registerOnValidatorChange(fn: () => void): void {
|
||||
this._onChange = fn;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Provider which adds `MaxValidator` to the `NG_VALIDATORS` multi-provider list.
|
||||
*/
|
||||
export const MAX_VALIDATOR: StaticProvider = {
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => MaxValidator),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* A directive which installs the {@link MaxValidator} for any `formControlName`,
|
||||
* `formControl`, or control with `ngModel` that also has a `max` attribute.
|
||||
*
|
||||
* @see [Form Validation](guide/form-validation)
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* ### Adding a max validator
|
||||
*
|
||||
* The following example shows how to add a max validator to an input attached to an
|
||||
* ngModel binding.
|
||||
*
|
||||
* ```html
|
||||
* <input type="number" ngModel max="4">
|
||||
* ```
|
||||
*
|
||||
* @ngModule ReactiveFormsModule
|
||||
* @ngModule FormsModule
|
||||
* @publicApi
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]',
|
||||
providers: [MAX_VALIDATOR],
|
||||
host: {'[attr.max]': 'max ? max : null'}
|
||||
})
|
||||
export class MaxValidator extends AbstractValidatorDirective implements OnChanges {
|
||||
/**
|
||||
* @description
|
||||
* Tracks changes to the max bound to this directive.
|
||||
*/
|
||||
@Input() max!: string|number;
|
||||
/** @internal */
|
||||
inputName = 'max';
|
||||
/** @internal */
|
||||
normalizeInput = (input: string): number => parseInt(input, 10);
|
||||
/** @internal */
|
||||
createValidator = (max: number): ValidatorFn => Validators.max(max);
|
||||
/**
|
||||
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
|
||||
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
|
||||
* AOT mode. This could be refactored once ViewEngine is removed.
|
||||
* @nodoc
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.handleChanges(changes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Provider which adds `MinValidator` to the `NG_VALIDATORS` multi-provider list.
|
||||
*/
|
||||
export const MIN_VALIDATOR: StaticProvider = {
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => MinValidator),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* A directive which installs the {@link MinValidator} for any `formControlName`,
|
||||
* `formControl`, or control with `ngModel` that also has a `min` attribute.
|
||||
*
|
||||
* @see [Form Validation](guide/form-validation)
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* ### Adding a min validator
|
||||
*
|
||||
* The following example shows how to add a min validator to an input attached to an
|
||||
* ngModel binding.
|
||||
*
|
||||
* ```html
|
||||
* <input type="number" ngModel min="4">
|
||||
* ```
|
||||
*
|
||||
* @ngModule ReactiveFormsModule
|
||||
* @ngModule FormsModule
|
||||
* @publicApi
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]',
|
||||
providers: [MIN_VALIDATOR],
|
||||
host: {'[attr.min]': 'min ? min : null'}
|
||||
})
|
||||
export class MinValidator extends AbstractValidatorDirective implements OnChanges {
|
||||
/**
|
||||
* @description
|
||||
* Tracks changes to the min bound to this directive.
|
||||
*/
|
||||
@Input() min!: string|number;
|
||||
/** @internal */
|
||||
inputName = 'min';
|
||||
/** @internal */
|
||||
normalizeInput = (input: string): number => parseInt(input, 10);
|
||||
/** @internal */
|
||||
createValidator = (min: number): ValidatorFn => Validators.min(min);
|
||||
/**
|
||||
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
|
||||
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
|
||||
* AOT mode. This could be refactored once ViewEngine is removed.
|
||||
* @nodoc
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.handleChanges(changes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* An interface implemented by classes that perform asynchronous validation.
|
||||
|
|
|
@ -43,7 +43,7 @@ export {FormGroupName} from './directives/reactive_directives/form_group_name';
|
|||
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
|
||||
export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
|
||||
export {ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor';
|
||||
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
|
||||
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
|
||||
export {FormBuilder} from './form_builder';
|
||||
export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormGroup} from './model';
|
||||
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';
|
||||
|
|
|
@ -10,7 +10,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 {expect} from '@angular/core/testing/src/testing_internal';
|
||||
import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, ControlValueAccessor, DefaultValueAccessor, FormArray, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormsModule, 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, 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';
|
||||
|
@ -2346,6 +2346,211 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
|
|||
expect(resultArr.length)
|
||||
.toEqual(2, `Expected original observable to be canceled on the next value change.`);
|
||||
}));
|
||||
|
||||
describe('min and max validators', () => {
|
||||
function getComponent(dir: string): Type<MinMaxFormControlComp|MinMaxFormControlNameComp> {
|
||||
return dir === 'formControl' ? MinMaxFormControlComp : MinMaxFormControlNameComp;
|
||||
}
|
||||
// Run tests for both `FormControlName` and `FormControl` directives
|
||||
['formControl', 'formControlName'].forEach((dir: string) => {
|
||||
it('should validate max', () => {
|
||||
const fixture = initTest(getComponent(dir));
|
||||
const control = new FormControl(5);
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.componentInstance.form = new FormGroup({'pin': control});
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.componentInstance.form;
|
||||
|
||||
expect(input.value).toEqual('5');
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
input.value = 2;
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: 2});
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
fixture.componentInstance.max = 1;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({max: {max: 1, actual: 2}});
|
||||
});
|
||||
|
||||
it('should apply max validation when control value is defined as a string', () => {
|
||||
const fixture = initTest(getComponent(dir));
|
||||
const control = new FormControl('5');
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.componentInstance.form = new FormGroup({'pin': control});
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.componentInstance.form;
|
||||
|
||||
expect(input.value).toEqual('5');
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
input.value = '2';
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: 2});
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
fixture.componentInstance.max = 1;
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({max: {max: 1, actual: 2}});
|
||||
});
|
||||
|
||||
it('should validate min', () => {
|
||||
const fixture = initTest(getComponent(dir));
|
||||
const control = new FormControl(5);
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.componentInstance.form = new FormGroup({'pin': control});
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.componentInstance.form;
|
||||
|
||||
expect(input.value).toEqual('5');
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
input.value = 2;
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: 2});
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
fixture.componentInstance.min = 5;
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({min: {min: 5, actual: 2}});
|
||||
});
|
||||
|
||||
it('should apply min validation when control value is defined as a string', () => {
|
||||
const fixture = initTest(getComponent(dir));
|
||||
const control = new FormControl('5');
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.componentInstance.form = new FormGroup({'pin': control});
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.componentInstance.form;
|
||||
|
||||
expect(input.value).toEqual('5');
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
input.value = '2';
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: 2});
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
fixture.componentInstance.min = 5;
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({min: {min: 5, actual: 2}});
|
||||
});
|
||||
|
||||
it('should run min/max validation for empty values', () => {
|
||||
const fixture = initTest(getComponent(dir));
|
||||
const minValidateFnSpy = spyOn(MinValidator.prototype, 'validate');
|
||||
const maxValidateFnSpy = spyOn(MaxValidator.prototype, 'validate');
|
||||
|
||||
const control = new FormControl();
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.componentInstance.form = new FormGroup({'pin': control});
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.componentInstance.form;
|
||||
|
||||
expect(input.value).toEqual('');
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
expect(minValidateFnSpy).toHaveBeenCalled();
|
||||
expect(maxValidateFnSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should run min/max validation when constraints are represented as strings', () => {
|
||||
const fixture = initTest(getComponent(dir));
|
||||
const control = new FormControl(5);
|
||||
|
||||
// Run tests when min and max are defined as strings.
|
||||
fixture.componentInstance.min = '1';
|
||||
fixture.componentInstance.max = '10';
|
||||
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.componentInstance.form = new FormGroup({'pin': control});
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.componentInstance.form;
|
||||
|
||||
expect(input.value).toEqual('5');
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
input.value = 2; // inside [1, 10] range
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: 2});
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
input.value = -2; // outside [1, 10] range
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: -2});
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({min: {min: 1, actual: -2}});
|
||||
|
||||
input.value = 20; // outside [1, 10] range
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({max: {max: 10, actual: 20}});
|
||||
});
|
||||
|
||||
it('should run min/max validation for negative values', () => {
|
||||
const fixture = initTest(getComponent(dir));
|
||||
const control = new FormControl(-30);
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.componentInstance.form = new FormGroup({'pin': control});
|
||||
fixture.componentInstance.min = -20;
|
||||
fixture.componentInstance.max = -10;
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.componentInstance.form;
|
||||
|
||||
expect(input.value).toEqual('-30');
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({min: {min: -20, actual: -30}});
|
||||
|
||||
input.value = -15;
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: -15});
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.pin.errors).toBeNull();
|
||||
|
||||
input.value = -5;
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: -5});
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({max: {max: -10, actual: -5}});
|
||||
|
||||
input.value = 0;
|
||||
dispatchEvent(input, 'input');
|
||||
expect(form.value).toEqual({pin: 0});
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.pin.errors).toEqual({max: {max: -10, actual: 0}});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
|
@ -4461,3 +4666,31 @@ class NgForFormControlWithValidators {
|
|||
form = new FormGroup({login: new FormControl('a')});
|
||||
logins = ['a', 'b', 'c'];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'min-max-form-control-name',
|
||||
template: `
|
||||
<div [formGroup]="form">
|
||||
<input type="number" formControlName="pin" [max]="max" [min]="min">
|
||||
</div>`
|
||||
})
|
||||
class MinMaxFormControlNameComp {
|
||||
control!: FormControl;
|
||||
form!: FormGroup;
|
||||
min: number|string = 1;
|
||||
max: number|string = 10;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'min-max-form-control',
|
||||
template: `
|
||||
<div [formGroup]="form">
|
||||
<input type="number" [formControl]="control" [max]="max" [min]="min">
|
||||
</div>`
|
||||
})
|
||||
class MinMaxFormControlComp {
|
||||
control!: FormControl;
|
||||
form!: FormGroup;
|
||||
min: number|string = 1;
|
||||
max: number|string = 10;
|
||||
}
|
|
@ -7,9 +7,9 @@
|
|||
*/
|
||||
|
||||
import {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {Component, Directive, forwardRef, Type} from '@angular/core';
|
||||
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, FormControl, FormsModule, NG_ASYNC_VALIDATORS, NgForm, NgModel} from '@angular/forms';
|
||||
import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, ControlValueAccessor, FormControl, FormsModule, MaxValidator, MinValidator, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, NgModel} 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';
|
||||
|
@ -1511,6 +1511,462 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
|
|||
expect(onNgModelChange).toHaveBeenCalledTimes(2);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should validate max', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMaxValidator);
|
||||
fixture.componentInstance.max = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.max.errors).toBeNull();
|
||||
|
||||
input.value = 11;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.max.errors).toEqual({max: {max: 10, actual: 11}});
|
||||
|
||||
input.value = 9;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.max.errors).toBeNull();
|
||||
}));
|
||||
|
||||
it('should apply max validation when control value is defined as a string', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMaxValidator);
|
||||
fixture.componentInstance.max = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '11';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.max.errors).toEqual({max: {max: 10, actual: 11}});
|
||||
|
||||
input.value = '9';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.max.errors).toBeNull();
|
||||
}));
|
||||
|
||||
it('should re-validate if max changes', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMaxValidator);
|
||||
fixture.componentInstance.max = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = 11;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.max.errors).toEqual({max: {max: 10, actual: 11}});
|
||||
|
||||
input.value = 9;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.max.errors).toBeNull();
|
||||
|
||||
fixture.componentInstance.max = 5;
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.max.errors).toEqual({max: {max: 5, actual: 9}});
|
||||
}));
|
||||
|
||||
it('should validate min', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinValidator);
|
||||
fixture.componentInstance.min = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min.errors).toBeNull();
|
||||
|
||||
input.value = 11;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min.errors).toBeNull();
|
||||
|
||||
input.value = 9;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min.errors).toEqual({min: {min: 10, actual: 9}});
|
||||
}));
|
||||
|
||||
it('should apply min validation when control value is defined as a string', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinValidator);
|
||||
fixture.componentInstance.min = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '11';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min.errors).toBeNull();
|
||||
|
||||
input.value = '9';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min.errors).toEqual({min: {min: 10, actual: 9}});
|
||||
}));
|
||||
|
||||
it('should re-validate if min changes', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinValidator);
|
||||
fixture.componentInstance.min = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = 11;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min.errors).toBeNull();
|
||||
|
||||
input.value = 9;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min.errors).toEqual({min: {min: 10, actual: 9}});
|
||||
|
||||
fixture.componentInstance.min = 9;
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min.errors).toBeNull();
|
||||
}));
|
||||
|
||||
it('should not include the min and max validators when using another directive with the same properties',
|
||||
fakeAsync(() => {
|
||||
const fixture = initTest(NgModelNoMinMaxValidator);
|
||||
const validateFnSpy = spyOn(MaxValidator.prototype, 'validate');
|
||||
|
||||
fixture.componentInstance.min = 10;
|
||||
fixture.componentInstance.max = 20;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const min = fixture.debugElement.query(By.directive(MinValidator));
|
||||
expect(min).toBeNull();
|
||||
|
||||
const max = fixture.debugElement.query(By.directive(MaxValidator));
|
||||
expect(max).toBeNull();
|
||||
|
||||
const cd = fixture.debugElement.query(By.directive(CustomDirective));
|
||||
expect(cd).toBeDefined();
|
||||
|
||||
expect(validateFnSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not include the min and max validators when using a custom component with the same properties',
|
||||
fakeAsync(() => {
|
||||
@Directive({
|
||||
selector: 'my-custom-component',
|
||||
providers: [{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: forwardRef(() => MyCustomComponentDirective),
|
||||
}]
|
||||
})
|
||||
class MyCustomComponentDirective implements ControlValueAccessor {
|
||||
@Input() min!: number;
|
||||
@Input() max!: number;
|
||||
|
||||
writeValue(obj: any): void {}
|
||||
registerOnChange(fn: any): void {}
|
||||
registerOnTouched(fn: any): void {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<!-- no min/max validators should be matched on these elements -->
|
||||
<my-custom-component name="min" ngModel [min]="min"></my-custom-component>
|
||||
<my-custom-component name="max" ngModel [max]="max"></my-custom-component>
|
||||
`
|
||||
})
|
||||
class AppComponent {
|
||||
}
|
||||
|
||||
const fixture = initTest(AppComponent, MyCustomComponentDirective);
|
||||
const validateFnSpy = spyOn(MaxValidator.prototype, 'validate');
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const mv = fixture.debugElement.query(By.directive(MaxValidator));
|
||||
expect(mv).toBeNull();
|
||||
|
||||
const cd = fixture.debugElement.query(By.directive(CustomDirective));
|
||||
expect(cd).toBeDefined();
|
||||
|
||||
expect(validateFnSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not include the min and max validators for inputs with type range',
|
||||
fakeAsync(() => {
|
||||
@Component({template: '<input type="range" min="10" max="20">'})
|
||||
class AppComponent {
|
||||
}
|
||||
|
||||
const fixture = initTest(AppComponent);
|
||||
const maxValidateFnSpy = spyOn(MaxValidator.prototype, 'validate');
|
||||
const minValidateFnSpy = spyOn(MinValidator.prototype, 'validate');
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const maxValidator = fixture.debugElement.query(By.directive(MaxValidator));
|
||||
expect(maxValidator).toBeNull();
|
||||
|
||||
const minValidator = fixture.debugElement.query(By.directive(MinValidator));
|
||||
expect(minValidator).toBeNull();
|
||||
|
||||
expect(maxValidateFnSpy).not.toHaveBeenCalled();
|
||||
expect(minValidateFnSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
['number', 'string'].forEach((inputType: string) => {
|
||||
it(`should validate min and max when constraints are represented using a ${inputType}`,
|
||||
fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinMaxValidator);
|
||||
|
||||
fixture.componentInstance.min = inputType === 'string' ? '5' : 5;
|
||||
fixture.componentInstance.max = inputType === 'string' ? '10' : 10;
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
input.value = 11;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({max: {max: 10, actual: 11}});
|
||||
|
||||
input.value = 4;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({min: {min: 5, actual: 4}});
|
||||
|
||||
input.value = 9;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
}));
|
||||
});
|
||||
it('should validate min and max', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinMaxValidator);
|
||||
fixture.componentInstance.min = 5;
|
||||
fixture.componentInstance.max = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
input.value = 11;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({max: {max: 10, actual: 11}});
|
||||
|
||||
input.value = 4;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({min: {min: 5, actual: 4}});
|
||||
|
||||
input.value = 9;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
}));
|
||||
|
||||
it('should apply min and max validation when control value is defined as a string',
|
||||
fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinMaxValidator);
|
||||
fixture.componentInstance.min = 5;
|
||||
fixture.componentInstance.max = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
input.value = '11';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({max: {max: 10, actual: 11}});
|
||||
|
||||
input.value = '4';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({min: {min: 5, actual: 4}});
|
||||
|
||||
input.value = '9';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
}));
|
||||
|
||||
it('should re-validate if min/max changes', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinMaxValidator);
|
||||
fixture.componentInstance.min = 5;
|
||||
fixture.componentInstance.max = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = 10;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
input.value = 12;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({max: {max: 10, actual: 12}});
|
||||
|
||||
fixture.componentInstance.max = 12;
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
input.value = 5;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
input.value = 0;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(false);
|
||||
expect(form.controls.min_max.errors).toEqual({min: {min: 5, actual: 0}});
|
||||
|
||||
fixture.componentInstance.min = 0;
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
}));
|
||||
|
||||
it('should run min/max validation for empty values ', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinMaxValidator);
|
||||
fixture.componentInstance.min = 5;
|
||||
fixture.componentInstance.max = 10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
const maxValidateFnSpy = spyOn(MaxValidator.prototype, 'validate');
|
||||
const minValidateFnSpy = spyOn(MinValidator.prototype, 'validate');
|
||||
|
||||
input.value = '';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toEqual(true);
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
expect(maxValidateFnSpy).toHaveBeenCalled();
|
||||
expect(minValidateFnSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should run min/max validation for negative values', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelMinMaxValidator);
|
||||
fixture.componentInstance.min = -20;
|
||||
fixture.componentInstance.max = -10;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
input.value = '-30';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.min_max.errors).toEqual({min: {min: -20, actual: -30}});
|
||||
|
||||
input.value = -15;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toBeTruthy();
|
||||
expect(form.controls.min_max.errors).toBeNull();
|
||||
|
||||
input.value = -5;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.min_max.errors).toEqual({max: {max: -10, actual: -5}});
|
||||
|
||||
input.value = 0;
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
expect(form.valid).toBeFalse();
|
||||
expect(form.controls.min_max.errors).toEqual({max: {max: -10, actual: 0}});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('IME events', () => {
|
||||
|
@ -1879,3 +2335,50 @@ class NgModelChangesForm {
|
|||
class NgModelChangeState {
|
||||
onNgModelChange = () => {};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-model-max',
|
||||
template: `<form><input name="max" type="number" ngModel [max]="max"></form>`
|
||||
})
|
||||
class NgModelMaxValidator {
|
||||
max!: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-model-min',
|
||||
template: `<form><input name="min" type="number" ngModel [min]="min"></form>`
|
||||
})
|
||||
class NgModelMinValidator {
|
||||
min!: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-model-min-max',
|
||||
template: `
|
||||
<form><input name="min_max" type="number" ngModel [min]="min" [max]="max"></form>`
|
||||
})
|
||||
class NgModelMinMaxValidator {
|
||||
min!: number|string;
|
||||
max!: number|string;
|
||||
}
|
||||
|
||||
@Directive({selector: '[myDir]'})
|
||||
class CustomDirective {
|
||||
@Input() min!: number;
|
||||
@Input() max!: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-model-no-min-max',
|
||||
template: `
|
||||
<form>
|
||||
<input name="min" type="text" ngModel [min]="min" myDir>
|
||||
<input name="max" type="text" ngModel [max]="max" myDir>
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
class NgModelNoMinMaxValidator {
|
||||
min!: number;
|
||||
max!: number;
|
||||
@ViewChild('myDir') myDir: any;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue