feat(forms): add email validator (#13709)

Closes #13706

PR Close #13709
This commit is contained in:
Dzmitry Shylovich 2016-12-29 20:07:02 +03:00 committed by Miško Hevery
parent 00979838ef
commit d69717cf79
7 changed files with 120 additions and 2 deletions

View File

@ -24,7 +24,7 @@ import {FormGroupDirective} from './directives/reactive_directives/form_group_di
import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name'; import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
import {CheckboxRequiredValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators'; import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
export {ControlValueAccessor} from './directives/control_value_accessor'; export {ControlValueAccessor} from './directives/control_value_accessor';
@ -62,6 +62,7 @@ export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
MaxLengthValidator, MaxLengthValidator,
PatternValidator, PatternValidator,
CheckboxRequiredValidator, CheckboxRequiredValidator,
EmailValidator,
]; ];
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm]; export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];

View File

@ -106,6 +106,50 @@ export class CheckboxRequiredValidator extends RequiredValidator {
} }
} }
/**
* Provider which adds {@link EmailValidator} to {@link NG_VALIDATORS}.
*/
export const EMAIL_VALIDATOR: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => EmailValidator),
multi: true
};
/**
* A Directive that adds the `email` validator to controls marked with the
* `email` attribute, via the {@link NG_VALIDATORS} binding.
*
* ### Example
*
* ```
* <input type="email" name="email" ngModel email>
* <input type="email" name="email" ngModel email="true">
* <input type="email" name="email" ngModel [email]="true">
* ```
*
* @experimental
*/
@Directive({
selector: '[email][formControlName],[email][formControl],[email][ngModel]',
providers: [EMAIL_VALIDATOR]
})
export class EmailValidator implements Validator {
private _enabled: boolean;
private _onChange: () => void;
@Input()
set email(value: boolean|string) {
this._enabled = value === '' || value === true || value === 'true';
if (this._onChange) this._onChange();
}
validate(c: AbstractControl): {[key: string]: any} {
return this._enabled ? Validators.email(c) : null;
}
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
}
/** /**
* @stable * @stable
*/ */

View File

@ -38,7 +38,7 @@ export {FormArrayName} from './directives/reactive_directives/form_group_name';
export {FormGroupName} 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 {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
export {AsyncValidatorFn, CheckboxRequiredValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, Validator, ValidatorFn} from './directives/validators'; export {AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, Validator, ValidatorFn} from './directives/validators';
export {FormBuilder} from './form_builder'; export {FormBuilder} from './form_builder';
export {AbstractControl, FormArray, FormControl, FormGroup} from './model'; export {AbstractControl, FormArray, FormControl, FormGroup} from './model';
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators'; export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';

View File

@ -45,6 +45,9 @@ export const NG_VALIDATORS = new InjectionToken<Array<Validator|Function>>('NgVa
export const NG_ASYNC_VALIDATORS = export const NG_ASYNC_VALIDATORS =
new InjectionToken<Array<Validator|Function>>('NgAsyncValidators'); new InjectionToken<Array<Validator|Function>>('NgAsyncValidators');
const EMAIL_REGEXP =
/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
/** /**
* Provides a set of validators used by form controls. * Provides a set of validators used by form controls.
* *
@ -74,6 +77,13 @@ export class Validators {
return control.value === true ? null : {'required': true}; return control.value === true ? null : {'required': true};
} }
/**
* Validator that performs email validation.
*/
static email(control: AbstractControl): {[key: string]: boolean} {
return EMAIL_REGEXP.test(control.value) ? null : {'email': true};
}
/** /**
* Validator that requires controls to have a value of a minimum length. * Validator that requires controls to have a value of a minimum length.
*/ */

View File

@ -859,6 +859,41 @@ export function main() {
expect(control.hasError('required')).toBe(true); expect(control.hasError('required')).toBe(true);
})); }));
it('should validate email', fakeAsync(() => {
const fixture = initTest(NgModelEmailValidator);
fixture.detectChanges();
tick();
const control =
fixture.debugElement.children[0].injector.get(NgForm).control.get('email');
const input = fixture.debugElement.query(By.css('input'));
expect(control.hasError('email')).toBe(false);
fixture.componentInstance.validatorEnabled = true;
fixture.detectChanges();
tick();
expect(input.nativeElement.value).toEqual('');
expect(control.hasError('email')).toBe(true);
input.nativeElement.value = 'test@gmail.com';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
tick();
expect(input.nativeElement.value).toEqual('test@gmail.com');
expect(control.hasError('email')).toBe(false);
input.nativeElement.value = 'text';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
tick();
expect(input.nativeElement.value).toEqual('text');
expect(control.hasError('email')).toBe(true);
}));
it('should support dir validators using bindings', fakeAsync(() => { it('should support dir validators using bindings', fakeAsync(() => {
const fixture = initTest(NgModelValidationBindings); const fixture = initTest(NgModelValidationBindings);
fixture.componentInstance.required = true; fixture.componentInstance.required = true;
@ -1335,6 +1370,14 @@ class NgModelCheckboxRequiredValidator {
required: boolean = false; required: boolean = false;
} }
@Component({
selector: 'ng-model-email',
template: `<form><input type="email" ngModel [email]="validatorEnabled" name="email"></form>`
})
class NgModelEmailValidator {
validatorEnabled: boolean = false;
}
@Directive({ @Directive({
selector: '[ng-async-validator]', selector: '[ng-async-validator]',
providers: [ providers: [

View File

@ -64,6 +64,14 @@ export function main() {
() => expect(Validators.requiredTrue(new FormControl(true))).toBeNull()); () => expect(Validators.requiredTrue(new FormControl(true))).toBeNull());
}); });
describe('email', () => {
it('should error on invalid email',
() => expect(Validators.email(new FormControl('some text'))).toEqual({'email': true}));
it('should not error on valid email',
() => expect(Validators.email(new FormControl('test@gmail.com'))).toBeNull());
});
describe('minLength', () => { describe('minLength', () => {
it('should not error on an empty string', it('should not error on an empty string',
() => { expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); }); () => { expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); });

View File

@ -150,6 +150,15 @@ export declare class DefaultValueAccessor implements ControlValueAccessor {
writeValue(value: any): void; writeValue(value: any): void;
} }
/** @experimental */
export declare class EmailValidator implements Validator {
email: boolean | string;
registerOnValidatorChange(fn: () => void): void;
validate(c: AbstractControl): {
[key: string]: any;
};
}
/** @stable */ /** @stable */
export interface Form { export interface Form {
addControl(dir: NgControl): void; addControl(dir: NgControl): void;
@ -529,6 +538,9 @@ export interface ValidatorFn {
export declare class Validators { export declare class Validators {
static compose(validators: ValidatorFn[]): ValidatorFn; static compose(validators: ValidatorFn[]): ValidatorFn;
static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn; static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn;
static email(control: AbstractControl): {
[key: string]: boolean;
};
static maxLength(maxLength: number): ValidatorFn; static maxLength(maxLength: number): ValidatorFn;
static minLength(minLength: number): ValidatorFn; static minLength(minLength: number): ValidatorFn;
static nullValidator(c: AbstractControl): { static nullValidator(c: AbstractControl): {