From 0b665c0ece816fa5a54de9a778bf29122e32267d Mon Sep 17 00:00:00 2001 From: Javier Ros Date: Sat, 6 Aug 2016 08:53:41 +0200 Subject: [PATCH] feat(validations): add support to bind validation attributes This change enables to bind the validations attributes `required`, `minlength`, `maxlength` and `pattern`. Closes: #10505, #7393 --- .../forms/src/directives/validators.ts | 90 +++++++--- .../forms/test/reactive_integration_spec.ts | 167 +++++++++++++++++- tools/public_api_guard/forms/index.d.ts | 21 ++- 3 files changed, 245 insertions(+), 33 deletions(-) diff --git a/modules/@angular/forms/src/directives/validators.ts b/modules/@angular/forms/src/directives/validators.ts index 56391978dd..abfd16500b 100644 --- a/modules/@angular/forms/src/directives/validators.ts +++ b/modules/@angular/forms/src/directives/validators.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Attribute, Directive, forwardRef} from '@angular/core'; +import {Attribute, Directive, HostBinding, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core'; -import {NumberWrapper} from '../facade/lang'; +import {isPresent} from '../facade/lang'; import {AbstractControl} from '../model'; import {NG_VALIDATORS, Validators} from '../validators'; @@ -35,11 +35,9 @@ import {NG_VALIDATORS, Validators} from '../validators'; */ export interface Validator { validate(c: AbstractControl): {[key: string]: any}; } -export const REQUIRED = Validators.required; - export const REQUIRED_VALIDATOR: any = { provide: NG_VALIDATORS, - useValue: REQUIRED, + useExisting: forwardRef(() => RequiredValidator), multi: true }; @@ -57,9 +55,20 @@ export const REQUIRED_VALIDATOR: any = { */ @Directive({ selector: '[required][formControlName],[required][formControl],[required][ngModel]', - providers: [REQUIRED_VALIDATOR] + providers: [REQUIRED_VALIDATOR], + host: {'[attr.required]': 'required? "" : null'} }) -export class RequiredValidator { +export class RequiredValidator implements Validator { + private _required: boolean; + + @Input() + get required(): boolean { return this._required; } + + set required(value: boolean) { this._required = isPresent(value) && `${value}` !== 'false'; } + + validate(c: AbstractControl): {[key: string]: any} { + return this.required ? Validators.required(c) : null; + } } /** @@ -95,16 +104,29 @@ export const MIN_LENGTH_VALIDATOR: any = { */ @Directive({ selector: '[minlength][formControlName],[minlength][formControl],[minlength][ngModel]', - providers: [MIN_LENGTH_VALIDATOR] + providers: [MIN_LENGTH_VALIDATOR], + host: {'[attr.minlength]': 'minlength? minlength : null'} }) -export class MinLengthValidator implements Validator { +export class MinLengthValidator implements Validator, + OnChanges { private _validator: ValidatorFn; - constructor(@Attribute('minlength') minLength: string) { - this._validator = Validators.minLength(NumberWrapper.parseInt(minLength, 10)); + @Input() minlength: string; + + private _createValidator() { + this._validator = Validators.minLength(parseInt(this.minlength, 10)); } - validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); } + ngOnChanges(changes: SimpleChanges) { + const minlengthChange = changes['minlength']; + if (minlengthChange) { + this._createValidator(); + } + } + + validate(c: AbstractControl): {[key: string]: any} { + return isPresent(this.minlength) ? this._validator(c) : null; + } } /** @@ -129,16 +151,29 @@ export const MAX_LENGTH_VALIDATOR: any = { */ @Directive({ selector: '[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]', - providers: [MAX_LENGTH_VALIDATOR] + providers: [MAX_LENGTH_VALIDATOR], + host: {'[attr.maxlength]': 'maxlength? maxlength : null'} }) -export class MaxLengthValidator implements Validator { +export class MaxLengthValidator implements Validator, + OnChanges { private _validator: ValidatorFn; - constructor(@Attribute('maxlength') maxLength: string) { - this._validator = Validators.maxLength(NumberWrapper.parseInt(maxLength, 10)); + @Input() maxlength: string; + + private _createValidator() { + this._validator = Validators.maxLength(parseInt(this.maxlength, 10)); } - validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); } + ngOnChanges(changes: SimpleChanges) { + const maxlengthChange = changes['maxlength']; + if (maxlengthChange) { + this._createValidator(); + } + } + + validate(c: AbstractControl): {[key: string]: any} { + return isPresent(this.maxlength) ? this._validator(c) : null; + } } @@ -164,14 +199,25 @@ export const PATTERN_VALIDATOR: any = { */ @Directive({ selector: '[pattern][formControlName],[pattern][formControl],[pattern][ngModel]', - providers: [PATTERN_VALIDATOR] + providers: [PATTERN_VALIDATOR], + host: {'[attr.pattern]': 'pattern? pattern : null'} }) -export class PatternValidator implements Validator { +export class PatternValidator implements Validator, + OnChanges { private _validator: ValidatorFn; - constructor(@Attribute('pattern') pattern: string) { - this._validator = Validators.pattern(pattern); + @Input() pattern: string; + + private _createValidator() { this._validator = Validators.pattern(this.pattern); } + + ngOnChanges(changes: SimpleChanges) { + const patternChange = changes['pattern']; + if (patternChange) { + this._createValidator(); + } } - validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); } + validate(c: AbstractControl): {[key: string]: any} { + return isPresent(this.pattern) ? this._validator(c) : null; + } } diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index e3d21c5896..3a0df12547 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -27,8 +27,8 @@ export function main() { FormControlComp, FormGroupComp, FormArrayComp, FormArrayNestedGroup, FormControlNameSelect, FormControlNumberInput, FormControlRadioButtons, WrappedValue, WrappedValueForm, MyInput, MyInputForm, FormGroupNgModel, FormControlNgModel, - LoginIsEmptyValidator, LoginIsEmptyWrapper, UniqLoginValidator, UniqLoginWrapper, - NestedFormGroupComp + LoginIsEmptyValidator, LoginIsEmptyWrapper, ValidationBindingsForm, UniqLoginValidator, + UniqLoginWrapper, NestedFormGroupComp ] }); }); @@ -933,39 +933,177 @@ export function main() { describe('validations', () => { it('should use sync validators defined in html', () => { const fixture = TestBed.createComponent(LoginIsEmptyWrapper); - const form = new FormGroup( - {'login': new FormControl(''), 'min': new FormControl(''), 'max': new FormControl('')}); + const form = new FormGroup({ + 'login': new FormControl(''), + 'min': new FormControl(''), + 'max': new FormControl(''), + 'pattern': new FormControl('') + }); fixture.debugElement.componentInstance.form = form; fixture.detectChanges(); const required = fixture.debugElement.query(By.css('[required]')); const minLength = fixture.debugElement.query(By.css('[minlength]')); const maxLength = fixture.debugElement.query(By.css('[maxlength]')); + const pattern = fixture.debugElement.query(By.css('[pattern]')); required.nativeElement.value = ''; minLength.nativeElement.value = '1'; maxLength.nativeElement.value = '1234'; + pattern.nativeElement.value = '12'; dispatchEvent(required.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input'); + dispatchEvent(pattern.nativeElement, 'input'); expect(form.hasError('required', ['login'])).toEqual(true); expect(form.hasError('minlength', ['min'])).toEqual(true); expect(form.hasError('maxlength', ['max'])).toEqual(true); + expect(form.hasError('pattern', ['pattern'])).toEqual(true); expect(form.hasError('loginIsEmpty')).toEqual(true); required.nativeElement.value = '1'; minLength.nativeElement.value = '123'; maxLength.nativeElement.value = '123'; + pattern.nativeElement.value = '123'; dispatchEvent(required.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input'); + dispatchEvent(pattern.nativeElement, 'input'); expect(form.valid).toEqual(true); }); + it('should use sync validators using bindings', () => { + const fixture = TestBed.createComponent(ValidationBindingsForm); + const form = new FormGroup({ + 'login': new FormControl(''), + 'min': new FormControl(''), + 'max': new FormControl(''), + 'pattern': new FormControl('') + }); + fixture.debugElement.componentInstance.form = form; + fixture.debugElement.componentInstance.required = true; + fixture.debugElement.componentInstance.minLen = 3; + fixture.debugElement.componentInstance.maxLen = 3; + fixture.debugElement.componentInstance.pattern = '.{3,}'; + fixture.detectChanges(); + + const required = fixture.debugElement.query(By.css('[name=required]')); + const minLength = fixture.debugElement.query(By.css('[name=minlength]')); + const maxLength = fixture.debugElement.query(By.css('[name=maxlength]')); + const pattern = fixture.debugElement.query(By.css('[name=pattern]')); + + required.nativeElement.value = ''; + minLength.nativeElement.value = '1'; + maxLength.nativeElement.value = '1234'; + pattern.nativeElement.value = '12'; + + dispatchEvent(required.nativeElement, 'input'); + dispatchEvent(minLength.nativeElement, 'input'); + dispatchEvent(maxLength.nativeElement, 'input'); + dispatchEvent(pattern.nativeElement, 'input'); + + expect(form.hasError('required', ['login'])).toEqual(true); + expect(form.hasError('minlength', ['min'])).toEqual(true); + expect(form.hasError('maxlength', ['max'])).toEqual(true); + expect(form.hasError('pattern', ['pattern'])).toEqual(true); + + required.nativeElement.value = '1'; + minLength.nativeElement.value = '123'; + maxLength.nativeElement.value = '123'; + pattern.nativeElement.value = '123'; + + dispatchEvent(required.nativeElement, 'input'); + dispatchEvent(minLength.nativeElement, 'input'); + dispatchEvent(maxLength.nativeElement, 'input'); + dispatchEvent(pattern.nativeElement, 'input'); + + expect(form.valid).toEqual(true); + }); + + it('changes on binded properties should change the validation state of the form', () => { + const fixture = TestBed.createComponent(ValidationBindingsForm); + const form = new FormGroup({ + 'login': new FormControl(''), + 'min': new FormControl(''), + 'max': new FormControl(''), + 'pattern': new FormControl('') + }); + fixture.debugElement.componentInstance.form = form; + fixture.detectChanges(); + + const required = fixture.debugElement.query(By.css('[name=required]')); + const minLength = fixture.debugElement.query(By.css('[name=minlength]')); + const maxLength = fixture.debugElement.query(By.css('[name=maxlength]')); + const pattern = fixture.debugElement.query(By.css('[name=pattern]')); + + required.nativeElement.value = ''; + minLength.nativeElement.value = '1'; + maxLength.nativeElement.value = '1234'; + pattern.nativeElement.value = '12'; + + dispatchEvent(required.nativeElement, 'input'); + dispatchEvent(minLength.nativeElement, 'input'); + dispatchEvent(maxLength.nativeElement, 'input'); + dispatchEvent(pattern.nativeElement, 'input'); + + expect(form.hasError('required', ['login'])).toEqual(false); + expect(form.hasError('minlength', ['min'])).toEqual(false); + expect(form.hasError('maxlength', ['max'])).toEqual(false); + expect(form.hasError('pattern', ['pattern'])).toEqual(false); + expect(form.valid).toEqual(true); + + fixture.debugElement.componentInstance.required = true; + fixture.debugElement.componentInstance.minLen = 3; + fixture.debugElement.componentInstance.maxLen = 3; + fixture.debugElement.componentInstance.pattern = '.{3,}'; + fixture.detectChanges(); + + dispatchEvent(required.nativeElement, 'input'); + dispatchEvent(minLength.nativeElement, 'input'); + dispatchEvent(maxLength.nativeElement, 'input'); + dispatchEvent(pattern.nativeElement, 'input'); + + expect(form.hasError('required', ['login'])).toEqual(true); + expect(form.hasError('minlength', ['min'])).toEqual(true); + expect(form.hasError('maxlength', ['max'])).toEqual(true); + expect(form.hasError('pattern', ['pattern'])).toEqual(true); + expect(form.valid).toEqual(false); + + expect(required.nativeElement.getAttribute('required')).toEqual(''); + expect(fixture.debugElement.componentInstance.minLen.toString()) + .toEqual(minLength.nativeElement.getAttribute('minlength')); + expect(fixture.debugElement.componentInstance.maxLen.toString()) + .toEqual(maxLength.nativeElement.getAttribute('maxlength')); + expect(fixture.debugElement.componentInstance.pattern.toString()) + .toEqual(pattern.nativeElement.getAttribute('pattern')); + + fixture.debugElement.componentInstance.required = false; + fixture.debugElement.componentInstance.minLen = null; + fixture.debugElement.componentInstance.maxLen = null; + fixture.debugElement.componentInstance.pattern = null; + fixture.detectChanges(); + + dispatchEvent(required.nativeElement, 'input'); + dispatchEvent(minLength.nativeElement, 'input'); + dispatchEvent(maxLength.nativeElement, 'input'); + dispatchEvent(pattern.nativeElement, 'input'); + + expect(form.hasError('required', ['login'])).toEqual(false); + expect(form.hasError('minlength', ['min'])).toEqual(false); + expect(form.hasError('maxlength', ['max'])).toEqual(false); + expect(form.hasError('pattern', ['pattern'])).toEqual(false); + expect(form.valid).toEqual(true); + + expect(required.nativeElement.getAttribute('required')).toEqual(null); + expect(required.nativeElement.getAttribute('minlength')).toEqual(null); + expect(required.nativeElement.getAttribute('maxlength')).toEqual(null); + expect(required.nativeElement.getAttribute('pattern')).toEqual(null); + }); + it('should use async validators defined in the html', fakeAsync(() => { const fixture = TestBed.createComponent(UniqLoginWrapper); const form = new FormGroup({'login': new FormControl('')}); @@ -1486,12 +1624,33 @@ class FormControlNgModel { + ` }) class LoginIsEmptyWrapper { form: FormGroup; } + +@Component({ + selector: 'validation-bindings-form', + template: ` +
+ + + + +
+ ` +}) +class ValidationBindingsForm { + form: FormGroup; + required: boolean; + minLen: number; + maxLen: number; + pattern: string; +} + @Component({ selector: 'uniq-login-wrapper', template: ` diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index 27c55a266b..0439da1371 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -316,16 +316,18 @@ export declare class FormsModule { } /** @stable */ -export declare class MaxLengthValidator implements Validator { - constructor(maxLength: string); +export declare class MaxLengthValidator implements Validator, OnChanges { + maxlength: string; + ngOnChanges(changes: SimpleChanges): void; validate(c: AbstractControl): { [key: string]: any; }; } /** @stable */ -export declare class MinLengthValidator implements Validator { - constructor(minLength: string); +export declare class MinLengthValidator implements Validator, OnChanges { + minlength: string; + ngOnChanges(changes: SimpleChanges): void; validate(c: AbstractControl): { [key: string]: any; }; @@ -424,8 +426,9 @@ export declare class NgSelectOption implements OnDestroy { } /** @stable */ -export declare class PatternValidator implements Validator { - constructor(pattern: string); +export declare class PatternValidator implements Validator, OnChanges { + pattern: string; + ngOnChanges(changes: SimpleChanges): void; validate(c: AbstractControl): { [key: string]: any; }; @@ -436,7 +439,11 @@ export declare class ReactiveFormsModule { } /** @stable */ -export declare class RequiredValidator { +export declare class RequiredValidator implements Validator { + required: boolean; + validate(c: AbstractControl): { + [key: string]: any; + }; } /** @stable */