From d69717cf79c3141cdbd69b538a3aaded55a8f43b Mon Sep 17 00:00:00 2001 From: Dzmitry Shylovich Date: Thu, 29 Dec 2016 20:07:02 +0300 Subject: [PATCH] feat(forms): add email validator (#13709) Closes #13706 PR Close #13709 --- modules/@angular/forms/src/directives.ts | 3 +- .../forms/src/directives/validators.ts | 44 +++++++++++++++++++ modules/@angular/forms/src/forms.ts | 2 +- modules/@angular/forms/src/validators.ts | 10 +++++ .../forms/test/template_integration_spec.ts | 43 ++++++++++++++++++ .../@angular/forms/test/validators_spec.ts | 8 ++++ tools/public_api_guard/forms/index.d.ts | 12 +++++ 7 files changed, 120 insertions(+), 2 deletions(-) diff --git a/modules/@angular/forms/src/directives.ts b/modules/@angular/forms/src/directives.ts index 723c650e68..9ba153f5ff 100644 --- a/modules/@angular/forms/src/directives.ts +++ b/modules/@angular/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, 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 {ControlValueAccessor} from './directives/control_value_accessor'; @@ -62,6 +62,7 @@ export const SHARED_FORM_DIRECTIVES: Type[] = [ MaxLengthValidator, PatternValidator, CheckboxRequiredValidator, + EmailValidator, ]; export const TEMPLATE_DRIVEN_DIRECTIVES: Type[] = [NgModel, NgModelGroup, NgForm]; diff --git a/modules/@angular/forms/src/directives/validators.ts b/modules/@angular/forms/src/directives/validators.ts index fd8c5f3baa..cc2805e1fc 100644 --- a/modules/@angular/forms/src/directives/validators.ts +++ b/modules/@angular/forms/src/directives/validators.ts @@ -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 + * + * ``` + * + * + * + * ``` + * + * @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 */ diff --git a/modules/@angular/forms/src/forms.ts b/modules/@angular/forms/src/forms.ts index b776d120ce..9bfea3aea1 100644 --- a/modules/@angular/forms/src/forms.ts +++ b/modules/@angular/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 {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 {AbstractControl, FormArray, FormControl, FormGroup} from './model'; export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators'; diff --git a/modules/@angular/forms/src/validators.ts b/modules/@angular/forms/src/validators.ts index 6bd15db5bb..b1647d3f33 100644 --- a/modules/@angular/forms/src/validators.ts +++ b/modules/@angular/forms/src/validators.ts @@ -45,6 +45,9 @@ export const NG_VALIDATORS = new InjectionToken>('NgVa export const NG_ASYNC_VALIDATORS = new InjectionToken>('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. * @@ -74,6 +77,13 @@ export class Validators { 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. */ diff --git a/modules/@angular/forms/test/template_integration_spec.ts b/modules/@angular/forms/test/template_integration_spec.ts index c16b871b62..c94bc7f368 100644 --- a/modules/@angular/forms/test/template_integration_spec.ts +++ b/modules/@angular/forms/test/template_integration_spec.ts @@ -859,6 +859,41 @@ export function main() { 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(() => { const fixture = initTest(NgModelValidationBindings); fixture.componentInstance.required = true; @@ -1335,6 +1370,14 @@ class NgModelCheckboxRequiredValidator { required: boolean = false; } +@Component({ + selector: 'ng-model-email', + template: `
` +}) +class NgModelEmailValidator { + validatorEnabled: boolean = false; +} + @Directive({ selector: '[ng-async-validator]', providers: [ diff --git a/modules/@angular/forms/test/validators_spec.ts b/modules/@angular/forms/test/validators_spec.ts index d3b290c5e3..6b9d18af36 100644 --- a/modules/@angular/forms/test/validators_spec.ts +++ b/modules/@angular/forms/test/validators_spec.ts @@ -64,6 +64,14 @@ export function main() { () => 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', () => { it('should not error on an empty string', () => { expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); }); diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index 9ce723aaef..b52e5dd59e 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -150,6 +150,15 @@ export declare class DefaultValueAccessor implements ControlValueAccessor { 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 */ export interface Form { addControl(dir: NgControl): void; @@ -529,6 +538,9 @@ export interface ValidatorFn { export declare class Validators { static compose(validators: ValidatorFn[]): ValidatorFn; static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn; + static email(control: AbstractControl): { + [key: string]: boolean; + }; static maxLength(maxLength: number): ValidatorFn; static minLength(minLength: number): ValidatorFn; static nullValidator(c: AbstractControl): {