feat(validations): add support to bind validation attributes

This change enables to bind the validations attributes `required`,
`minlength`, `maxlength` and `pattern`.

Closes: #10505, #7393
This commit is contained in:
Javier Ros 2016-08-06 08:53:41 +02:00 committed by Victor Berchet
parent 875e66409c
commit 0b665c0ece
3 changed files with 245 additions and 33 deletions

View File

@ -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;
}
}

View File

@ -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 {
<input type="text" formControlName="login" required>
<input type="text" formControlName="min" minlength="3">
<input type="text" formControlName="max" maxlength="3">
<input type="text" formControlName="pattern" pattern=".{3,}">
</div>
`
})
class LoginIsEmptyWrapper {
form: FormGroup;
}
@Component({
selector: 'validation-bindings-form',
template: `
<div [formGroup]="form">
<input name="required" type="text" formControlName="login" [required]="required">
<input name="minlength" type="text" formControlName="min" [minlength]="minLen">
<input name="maxlength" type="text" formControlName="max" [maxlength]="maxLen">
<input name="pattern" type="text" formControlName="pattern" [pattern]="pattern">
</div>
`
})
class ValidationBindingsForm {
form: FormGroup;
required: boolean;
minLen: number;
maxLen: number;
pattern: string;
}
@Component({
selector: 'uniq-login-wrapper',
template: `

View File

@ -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 */