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:
parent
875e66409c
commit
0b665c0ece
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: `
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Reference in New Issue