diff --git a/packages/forms/src/directives.ts b/packages/forms/src/directives.ts index 41a298e90e..101a6103c9 100644 --- a/packages/forms/src/directives.ts +++ b/packages/forms/src/directives.ts @@ -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'; @@ -58,7 +58,9 @@ export const SHARED_FORM_DIRECTIVES: Type[] = [ NgControlStatus, NgControlStatusGroup, RequiredValidator, + MinValidator, MinLengthValidator, + MaxValidator, MaxLengthValidator, PatternValidator, CheckboxRequiredValidator, diff --git a/packages/forms/src/directives/validators.ts b/packages/forms/src/directives/validators.ts index 92671bb147..7dddf3a3c3 100644 --- a/packages/forms/src/directives/validators.ts +++ b/packages/forms/src/directives/validators.ts @@ -57,6 +57,7 @@ export const CHECKBOX_REQUIRED_VALIDATOR: Provider = { multi: true }; + /** * A Directive that adds the `required` validator to any controls marked with the * `required` attribute, via the {@link NG_VALIDATORS} binding. @@ -94,6 +95,84 @@ export class RequiredValidator implements Validator { registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } } +export const MIN_VALIDATOR: Provider = { + 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. + * + * @experimental + */ +@Directive({ + selector: '[min][formControlName],[min][formControl],[min][ngModel]', + providers: [MIN_VALIDATOR], + host: {'[attr.min]': 'min ? min : null'} +}) +export class MinValidator implements Validator, + OnChanges { + private _validator: ValidatorFn; + private _onChange: () => void; + + @Input() min: string; + + ngOnChanges(changes: SimpleChanges): void { + if ('min' in changes) { + this._createValidator(); + if (this._onChange) this._onChange(); + } + } + + validate(c: AbstractControl): ValidationErrors|null { return this._validator(c); } + + registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + + private _createValidator(): void { this._validator = Validators.min(parseInt(this.min, 10)); } +} + + +export const MAX_VALIDATOR: Provider = { + 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 `min` attribute. + * + * @experimental + */ +@Directive({ + selector: '[max][formControlName],[max][formControl],[max][ngModel]', + providers: [MAX_VALIDATOR], + host: {'[attr.max]': 'max ? max : null'} +}) +export class MaxValidator implements Validator, + OnChanges { + private _validator: ValidatorFn; + private _onChange: () => void; + + @Input() max: string; + + ngOnChanges(changes: SimpleChanges): void { + if ('max' in changes) { + this._createValidator(); + if (this._onChange) this._onChange(); + } + } + + validate(c: AbstractControl): ValidationErrors|null { return this._validator(c); } + + registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + + private _createValidator(): void { this._validator = Validators.max(parseInt(this.max, 10)); } +} + + /** * A Directive that adds the `required` validator to checkbox controls marked with the * `required` attribute, via the {@link NG_VALIDATORS} binding. diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 81caaa2dca..b1f882d1d4 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -38,7 +38,7 @@ export {FormArrayName} from './directives/reactive_directives/form_group_name'; 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 {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, FormArray, FormControl, FormGroup} from './model'; export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators'; diff --git a/packages/forms/src/validators.ts b/packages/forms/src/validators.ts index de355fcbd4..820b24a22f 100644 --- a/packages/forms/src/validators.ts +++ b/packages/forms/src/validators.ts @@ -62,6 +62,32 @@ const EMAIL_REGEXP = * @stable */ export class Validators { + /** + * Validator that requires controls to have a value greater than a number. + */ + static min(min: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const value = parseFloat(control.value); + return isNaN(value) || value < min ? {'min': {'min': min, 'actual': control.value}} : null; + }; + } + + /** + * Validator that requires controls to have a value less than a number. + */ + static max(max: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const value = parseFloat(control.value); + return isNaN(value) || value > max ? {'max': {'max': max, 'actual': control.value}} : null; + }; + } + /** * Validator that requires controls to have a non-empty value. */ diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index 87fd77bdf8..9368dd859a 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -879,6 +879,97 @@ export function main() { describe('validation directives', () => { + it('should should validate max', fakeAsync(() => { + const fixture = initTest(NgModelMaxValidator); + fixture.componentInstance.max = 10; + fixture.detectChanges(); + tick(); + + const max = fixture.debugElement.query(By.css('input')); + const form = fixture.debugElement.children[0].injector.get(NgForm); + + max.nativeElement.value = ''; + dispatchEvent(max.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(true); + + max.nativeElement.value = 11; + dispatchEvent(max.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(false); + + max.nativeElement.value = 9; + dispatchEvent(max.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(true); + })); + + it('should validate max for strings', fakeAsync(() => { + const fixture = initTest(NgModelMaxValidator); + fixture.componentInstance.max = 10; + fixture.detectChanges(); + tick(); + + const max = fixture.debugElement.query(By.css('input')); + const form = fixture.debugElement.children[0].injector.get(NgForm); + + max.nativeElement.value = '11'; + dispatchEvent(max.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(false); + + max.nativeElement.value = '9'; + dispatchEvent(max.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(true); + })); + + it('should should validate min', fakeAsync(() => { + const fixture = initTest(NgModelMinValidator); + fixture.componentInstance.min = 10; + fixture.detectChanges(); + tick(); + + const min = fixture.debugElement.query(By.css('input')); + const form = fixture.debugElement.children[0].injector.get(NgForm); + + min.nativeElement.value = ''; + dispatchEvent(min.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(true); + + min.nativeElement.value = 11; + dispatchEvent(min.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(true); + + min.nativeElement.value = 9; + dispatchEvent(min.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(false); + })); + + it('should should validate min for strings', fakeAsync(() => { + const fixture = initTest(NgModelMinValidator); + fixture.componentInstance.min = 10; + fixture.detectChanges(); + tick(); + + const min = fixture.debugElement.query(By.css('input')); + const form = fixture.debugElement.children[0].injector.get(NgForm); + + min.nativeElement.value = '11'; + dispatchEvent(min.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(true); + + min.nativeElement.value = '9'; + dispatchEvent(min.nativeElement, 'input'); + fixture.detectChanges(); + expect(form.valid).toEqual(false); + })); + + it('required validator should validate checkbox', fakeAsync(() => { const fixture = initTest(NgModelCheckboxRequiredValidator); fixture.detectChanges(); @@ -1539,6 +1630,22 @@ class NgModelMultipleValidators { pattern: string|RegExp; } +@Component({ + selector: 'ng-model-max', + template: `
` +}) +class NgModelMaxValidator { + max: number; +} + +@Component({ + selector: 'ng-model-min', + template: `
` +}) +class NgModelMinValidator { + min: number; +} + @Component({ selector: 'ng-model-checkbox-validator', template: diff --git a/packages/forms/test/validators_spec.ts b/packages/forms/test/validators_spec.ts index b44c8ce7c0..e87f57172c 100644 --- a/packages/forms/test/validators_spec.ts +++ b/packages/forms/test/validators_spec.ts @@ -39,6 +39,65 @@ export function main() { } describe('Validators', () => { + describe('min', () => { + it('should not error on an empty string', + () => { expect(Validators.min(2)(new FormControl(''))).toBeNull(); }); + + it('should not error on null', + () => { expect(Validators.min(2)(new FormControl(null))).toBeNull(); }); + + it('should not error on undefined', + () => { expect(Validators.min(2)(new FormControl(undefined))).toBeNull(); }); + + it('should error on non numbers', () => { + expect(Validators.min(2)(new FormControl('a'))).toEqual({'min': {'min': 2, 'actual': 'a'}}); + }); + + it('should error on small values', () => { + expect(Validators.min(2)(new FormControl(1))).toEqual({'min': {'min': 2, 'actual': 1}}); + }); + + it('should not error on big values', + () => { expect(Validators.min(2)(new FormControl(3))).toBeNull(); }); + + it('should not error on equal values', + () => { expect(Validators.min(2)(new FormControl(2))).toBeNull(); }); + + it('should not error on equal values when value is string', + () => { expect(Validators.min(2)(new FormControl('2'))).toBeNull(); }); + }); + + describe('max', () => { + it('should not error on an empty string', + () => { expect(Validators.max(2)(new FormControl(''))).toBeNull(); }); + + it('should not error on null', + () => { expect(Validators.max(2)(new FormControl(null))).toBeNull(); }); + + it('should not error on undefined', + () => { expect(Validators.max(2)(new FormControl(undefined))).toBeNull(); }); + + it('should error on non numbers', () => { + expect(Validators.max(2)(new FormControl('aaa'))).toEqual({ + 'max': {'max': 2, 'actual': 'aaa'} + }); + }); + + it('should error on big values', () => { + expect(Validators.max(2)(new FormControl(3))).toEqual({'max': {'max': 2, 'actual': 3}}); + }); + + it('should not error on small values', + () => { expect(Validators.max(2)(new FormControl(1))).toBeNull(); }); + + it('should not error on equal values', + () => { expect(Validators.max(2)(new FormControl(2))).toBeNull(); }); + + it('should not error on equal values when value is string', + () => { expect(Validators.max(2)(new FormControl('2'))).toBeNull(); }); + }); + + describe('required', () => { it('should error on an empty string', () => { expect(Validators.required(new FormControl(''))).toEqual({'required': true}); }); diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index fdfc00298b..96f7d3f180 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -352,6 +352,14 @@ export declare class MaxLengthValidator implements Validator, OnChanges { validate(c: AbstractControl): ValidationErrors | null; } +/** @experimental */ +export declare class MaxValidator implements Validator, OnChanges { + max: string; + ngOnChanges(changes: SimpleChanges): void; + registerOnValidatorChange(fn: () => void): void; + validate(c: AbstractControl): ValidationErrors | null; +} + /** @stable */ export declare class MinLengthValidator implements Validator, OnChanges { minlength: string; @@ -360,6 +368,14 @@ export declare class MinLengthValidator implements Validator, OnChanges { validate(c: AbstractControl): ValidationErrors | null; } +/** @experimental */ +export declare class MinValidator implements Validator, OnChanges { + min: string; + ngOnChanges(changes: SimpleChanges): void; + registerOnValidatorChange(fn: () => void): void; + validate(c: AbstractControl): ValidationErrors | null; +} + /** @stable */ export declare const NG_ASYNC_VALIDATORS: InjectionToken<(Function | Validator)[]>; @@ -536,7 +552,9 @@ export declare class Validators { static compose(validators: (ValidatorFn | null | undefined)[]): ValidatorFn | null; static composeAsync(validators: (AsyncValidatorFn | null)[]): AsyncValidatorFn | null; static email(control: AbstractControl): ValidationErrors | null; + static max(max: number): ValidatorFn; static maxLength(maxLength: number): ValidatorFn; + static min(min: number): ValidatorFn; static minLength(minLength: number): ValidatorFn; static nullValidator(c: AbstractControl): ValidationErrors | null; static pattern(pattern: string | RegExp): ValidatorFn;