From 547e011abec8b3ca17b50cdea7815f61b0efb5d4 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 28 Oct 2015 16:54:27 -0700 Subject: [PATCH] feat(forms): add support for Validator Currently, the only way for a directive to export a validator is by providing a function. This makes it ackward to write validators that depend on directive inputs. In addition to supporting functions as validators, classes implementing the Validator interface are supported too. --- modules/angular2/src/core/forms.ts | 3 +- .../src/core/forms/directives/ng_control.ts | 5 +- .../core/forms/directives/ng_control_name.ts | 13 ++-- .../core/forms/directives/ng_form_control.ts | 13 ++-- .../src/core/forms/directives/ng_model.ts | 11 ++-- .../forms/directives/normalize_validator.dart | 12 ++++ .../forms/directives/normalize_validator.ts | 9 +++ .../src/core/forms/directives/shared.ts | 7 +++ .../src/core/forms/directives/validators.ts | 62 ++++++++++++------- .../test/core/forms/directives_spec.ts | 36 +++++++---- .../test/core/forms/integration_spec.ts | 3 + modules/angular2/test/public_api_spec.ts | 13 +--- 12 files changed, 123 insertions(+), 64 deletions(-) create mode 100644 modules/angular2/src/core/forms/directives/normalize_validator.dart create mode 100644 modules/angular2/src/core/forms/directives/normalize_validator.ts diff --git a/modules/angular2/src/core/forms.ts b/modules/angular2/src/core/forms.ts index 834a936de1..7cb45517ab 100644 --- a/modules/angular2/src/core/forms.ts +++ b/modules/angular2/src/core/forms.ts @@ -37,7 +37,8 @@ export {NG_VALIDATORS, Validators} from './forms/validators'; export { RequiredValidator, MinLengthValidator, - MaxLengthValidator + MaxLengthValidator, + Validator } from './forms/directives/validators'; export {FormBuilder} from './forms/form_builder'; diff --git a/modules/angular2/src/core/forms/directives/ng_control.ts b/modules/angular2/src/core/forms/directives/ng_control.ts index f33b77aa12..a2574aabca 100644 --- a/modules/angular2/src/core/forms/directives/ng_control.ts +++ b/modules/angular2/src/core/forms/directives/ng_control.ts @@ -1,5 +1,6 @@ import {ControlValueAccessor} from './control_value_accessor'; import {AbstractControlDirective} from './abstract_control_directive'; +import {unimplemented} from 'angular2/src/core/facade/exceptions'; /** * A base class that all control directive extend. @@ -13,7 +14,7 @@ export class NgControl extends AbstractControlDirective { name: string = null; valueAccessor: ControlValueAccessor = null; - get validator(): Function { return null; } + get validator(): Function { return unimplemented(); } - viewToModelUpdate(newValue: any): void {} + viewToModelUpdate(newValue: any): void { return unimplemented(); } } diff --git a/modules/angular2/src/core/forms/directives/ng_control_name.ts b/modules/angular2/src/core/forms/directives/ng_control_name.ts index e9a0fe5814..c8f3942396 100644 --- a/modules/angular2/src/core/forms/directives/ng_control_name.ts +++ b/modules/angular2/src/core/forms/directives/ng_control_name.ts @@ -8,7 +8,7 @@ import {forwardRef, Host, SkipSelf, Provider, Inject, Optional} from 'angular2/s import {ControlContainer} from './control_container'; import {NgControl} from './ng_control'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; -import {controlPath, isPropertyUpdated, selectValueAccessor} from './shared'; +import {controlPath, composeValidators, isPropertyUpdated, selectValueAccessor} from './shared'; import {Control} from '../model'; import {Validators, NG_VALIDATORS} from '../validators'; @@ -85,16 +85,17 @@ export class NgControlName extends NgControl implements OnChanges, update = new EventEmitter(); model: any; viewModel: any; - validators: Function[]; + private _validator: Function; /** @internal */ _added = false; constructor(@Host() @SkipSelf() parent: ControlContainer, - @Optional() @Inject(NG_VALIDATORS) validators: Function[], + @Optional() @Inject(NG_VALIDATORS) validators: + /* Array */ any[], @Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { super(); this._parent = parent; - this.validators = validators; + this._validator = composeValidators(validators); this.valueAccessor = selectValueAccessor(this, valueAccessors); } @@ -120,7 +121,7 @@ export class NgControlName extends NgControl implements OnChanges, get formDirective(): any { return this._parent.formDirective; } - get control(): Control { return this.formDirective.getControl(this); } + get validator(): Function { return this._validator; } - get validator(): Function { return Validators.compose(this.validators); } + get control(): Control { return this.formDirective.getControl(this); } } diff --git a/modules/angular2/src/core/forms/directives/ng_form_control.ts b/modules/angular2/src/core/forms/directives/ng_form_control.ts index d7107d0248..d7a809cfc5 100644 --- a/modules/angular2/src/core/forms/directives/ng_form_control.ts +++ b/modules/angular2/src/core/forms/directives/ng_form_control.ts @@ -9,7 +9,7 @@ import {NgControl} from './ng_control'; import {Control} from '../model'; import {Validators, NG_VALIDATORS} from '../validators'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; -import {setUpControl, isPropertyUpdated, selectValueAccessor} from './shared'; +import {setUpControl, composeValidators, isPropertyUpdated, selectValueAccessor} from './shared'; const formControlBinding = CONST_EXPR(new Provider(NgControl, {useExisting: forwardRef(() => NgFormControl)})); @@ -73,12 +73,13 @@ export class NgFormControl extends NgControl implements OnChanges { update = new EventEmitter(); model: any; viewModel: any; - validators: Function[]; + private _validator: Function; - constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[], + constructor(@Optional() @Inject(NG_VALIDATORS) validators: + /* Array */ any[], @Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { super(); - this.validators = validators; + this._validator = composeValidators(validators); this.valueAccessor = selectValueAccessor(this, valueAccessors); } @@ -95,9 +96,9 @@ export class NgFormControl extends NgControl implements OnChanges { get path(): string[] { return []; } - get control(): Control { return this.form; } + get validator(): Function { return this._validator; } - get validator(): Function { return Validators.compose(this.validators); } + get control(): Control { return this.form; } viewToModelUpdate(newValue: any): void { this.viewModel = newValue; diff --git a/modules/angular2/src/core/forms/directives/ng_model.ts b/modules/angular2/src/core/forms/directives/ng_model.ts index 8410abc643..50ce974c98 100644 --- a/modules/angular2/src/core/forms/directives/ng_model.ts +++ b/modules/angular2/src/core/forms/directives/ng_model.ts @@ -8,7 +8,7 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor' import {NgControl} from './ng_control'; import {Control} from '../model'; import {Validators, NG_VALIDATORS} from '../validators'; -import {setUpControl, isPropertyUpdated, selectValueAccessor} from './shared'; +import {setUpControl, isPropertyUpdated, selectValueAccessor, composeValidators} from './shared'; const formControlBinding = CONST_EXPR(new Provider(NgControl, {useExisting: forwardRef(() => NgModel)})); @@ -49,12 +49,13 @@ export class NgModel extends NgControl implements OnChanges { update = new EventEmitter(); model: any; viewModel: any; - validators: Function[]; + private _validator: Function; - constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[], + constructor(@Optional() @Inject(NG_VALIDATORS) validators: + /* Array */ any[], @Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { super(); - this.validators = validators; + this._validator = composeValidators(validators); this.valueAccessor = selectValueAccessor(this, valueAccessors); } @@ -75,7 +76,7 @@ export class NgModel extends NgControl implements OnChanges { get path(): string[] { return []; } - get validator(): Function { return Validators.compose(this.validators); } + get validator(): Function { return this._validator; } viewToModelUpdate(newValue: any): void { this.viewModel = newValue; diff --git a/modules/angular2/src/core/forms/directives/normalize_validator.dart b/modules/angular2/src/core/forms/directives/normalize_validator.dart new file mode 100644 index 0000000000..479096c6b1 --- /dev/null +++ b/modules/angular2/src/core/forms/directives/normalize_validator.dart @@ -0,0 +1,12 @@ +library angular2.core.forms.normalize_validators; + +import 'package:angular2/src/core/forms/directives/validators.dart' show Validator; + +Function normalizeValidator(dynamic validator){ + if (validator is Validator) { + return (c) => validator.validate(c); + } else { + return validator; + } +} + diff --git a/modules/angular2/src/core/forms/directives/normalize_validator.ts b/modules/angular2/src/core/forms/directives/normalize_validator.ts new file mode 100644 index 0000000000..88e3fa6a7b --- /dev/null +++ b/modules/angular2/src/core/forms/directives/normalize_validator.ts @@ -0,0 +1,9 @@ +import {Validator} from './validators'; + +export function normalizeValidator(validator: Function | Validator): Function { + if ((validator).validate !== undefined) { + return (c) => (validator).validate(c); + } else { + return validator; + } +} diff --git a/modules/angular2/src/core/forms/directives/shared.ts b/modules/angular2/src/core/forms/directives/shared.ts index 549c89e35d..cab892623e 100644 --- a/modules/angular2/src/core/forms/directives/shared.ts +++ b/modules/angular2/src/core/forms/directives/shared.ts @@ -15,6 +15,7 @@ import {DefaultValueAccessor} from './default_value_accessor'; import {NumberValueAccessor} from './number_value_accessor'; import {CheckboxControlValueAccessor} from './checkbox_value_accessor'; import {SelectControlValueAccessor} from './select_control_value_accessor'; +import {normalizeValidator} from './normalize_validator'; export function controlPath(name: string, parent: ControlContainer): string[] { @@ -59,6 +60,12 @@ export function setProperty(renderer: Renderer, elementRef: ElementRef, propName renderer.setElementProperty(elementRef, propName, propValue); } +export function composeValidators( + validators: /* Array */ any[]): Function { + return isPresent(validators) ? Validators.compose(validators.map(normalizeValidator)) : + Validators.nullValidator; +} + export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any): boolean { if (!StringMapWrapper.contains(changes, "model")) return false; var change = changes["model"]; diff --git a/modules/angular2/src/core/forms/directives/validators.ts b/modules/angular2/src/core/forms/directives/validators.ts index 8cb69da8c8..cfe33a4464 100644 --- a/modules/angular2/src/core/forms/directives/validators.ts +++ b/modules/angular2/src/core/forms/directives/validators.ts @@ -2,8 +2,30 @@ import {forwardRef, Provider, OpaqueToken} from 'angular2/src/core/di'; import {CONST_EXPR} from 'angular2/src/core/facade/lang'; import {Attribute, Directive} from 'angular2/src/core/metadata'; import {Validators, NG_VALIDATORS} from '../validators'; +import {Control} from '../model'; +import * as modelModule from '../model'; import {NumberWrapper} from "angular2/src/core/facade/lang"; + +/** + * An interface that can be implemented by classes that can act as validators. + * + * ## Usage + * + * ```typescript + * @Directive({ + * selector: '[custom-validator]', + * providers: [provide(NG_VALIDATORS, {useExisting: CustomValidatorDirective, multi: true})] + * }) + * class CustomValidatorDirective implements Validator { + * validate(c: Control): {[key: string]: any} { + * return {"custom": true}; + * } + * } + * ``` + */ +export interface Validator { validate(c: modelModule.Control): {[key: string]: any}; } + const REQUIRED_VALIDATOR = CONST_EXPR(new Provider(NG_VALIDATORS, {useValue: Validators.required, multi: true})); @@ -14,40 +36,34 @@ const REQUIRED_VALIDATOR = export class RequiredValidator { } -function createMinLengthValidator(dir): any { - return Validators.minLength(dir.minLength); -} -const MIN_LENGTH_VALIDATOR = CONST_EXPR(new Provider(NG_VALIDATORS, { - useFactory: createMinLengthValidator, - deps: [forwardRef(() => MinLengthValidator)], - multi: true -})); +const MIN_LENGTH_VALIDATOR = CONST_EXPR( + new Provider(NG_VALIDATORS, {useExisting: forwardRef(() => MinLengthValidator), multi: true})); @Directive({ selector: '[minlength][ng-control],[minlength][ng-form-control],[minlength][ng-model]', providers: [MIN_LENGTH_VALIDATOR] }) -export class MinLengthValidator { - minLength: number; +export class MinLengthValidator implements Validator { + private _validator: Function; + constructor(@Attribute("minlength") minLength: string) { - this.minLength = NumberWrapper.parseInt(minLength, 10); + this._validator = Validators.minLength(NumberWrapper.parseInt(minLength, 10)); } + + validate(c: Control): {[key: string]: any} { return this._validator(c); } } -function createMaxLengthValidator(dir): any { - return Validators.maxLength(dir.maxLength); -} -const MAX_LENGTH_VALIDATOR = CONST_EXPR(new Provider(NG_VALIDATORS, { - useFactory: createMaxLengthValidator, - deps: [forwardRef(() => MaxLengthValidator)], - multi: true -})); +const MAX_LENGTH_VALIDATOR = CONST_EXPR( + new Provider(NG_VALIDATORS, {useExisting: forwardRef(() => MaxLengthValidator), multi: true})); @Directive({ selector: '[maxlength][ng-control],[maxlength][ng-form-control],[maxlength][ng-model]', providers: [MAX_LENGTH_VALIDATOR] }) -export class MaxLengthValidator { - maxLength: number; - constructor(@Attribute("maxlength") maxLength: string) { - this.maxLength = NumberWrapper.parseInt(maxLength, 10); +export class MaxLengthValidator implements Validator { + private _validator: Function; + + constructor(@Attribute("maxlength") minLength: string) { + this._validator = Validators.maxLength(NumberWrapper.parseInt(minLength, 10)); } + + validate(c: Control): {[key: string]: any} { return this._validator(c); } } \ No newline at end of file diff --git a/modules/angular2/test/core/forms/directives_spec.ts b/modules/angular2/test/core/forms/directives_spec.ts index 2d855adaa0..d7212c6f3b 100644 --- a/modules/angular2/test/core/forms/directives_spec.ts +++ b/modules/angular2/test/core/forms/directives_spec.ts @@ -32,11 +32,12 @@ import { DefaultValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, - QueryList + QueryList, + Validator } from 'angular2/core'; -import {selectValueAccessor} from 'angular2/src/core/forms/directives/shared'; +import {selectValueAccessor, composeValidators} from 'angular2/src/core/forms/directives/shared'; import {SimpleChange} from 'angular2/src/core/change_detection'; @@ -49,6 +50,10 @@ class DummyControlValueAccessor implements ControlValueAccessor { writeValue(obj: any): void { this.writtenValue = obj; } } +class CustomValidatorDirective implements Validator { + validate(c: Control): {[key: string]: any} { return {"custom": true}; } +} + export function main() { describe("Form Directives", () => { var defaultAccessor; @@ -97,6 +102,21 @@ export function main() { expect(() => selectValueAccessor(dir, [customAccessor, customAccessor])).toThrowError(); }); }); + + describe("composeValidators", () => { + it("should compose functions", () => { + var dummy1 = (_) => ({"dummy1": true}); + var dummy2 = (_) => ({"dummy2": true}); + var v = composeValidators([dummy1, dummy2]); + expect(v(new Control(""))).toEqual({"dummy1": true, "dummy2": true}); + }); + + it("should compose validator directives", () => { + var dummy1 = (_) => ({"dummy1": true}); + var v = composeValidators([dummy1, new CustomValidatorDirective()]); + expect(v(new Control(""))).toEqual({"dummy1": true, "custom": true}); + }); + }); }); describe("NgFormModel", () => { @@ -113,7 +133,7 @@ export function main() { }); form.form = formModel; - loginControlDir = new NgControlName(form, [], [defaultAccessor]); + loginControlDir = new NgControlName(form, [Validators.required], [defaultAccessor]); loginControlDir.name = "login"; loginControlDir.valueAccessor = new DummyControlValueAccessor(); }); @@ -147,8 +167,6 @@ export function main() { }); it("should set up validator", () => { - loginControlDir.validators = [Validators.required]; - expect(formModel.find(["login"]).valid).toBe(true); // this will add the required validator and recalculate the validity @@ -335,7 +353,7 @@ export function main() { }; beforeEach(() => { - controlDir = new NgFormControl([], [defaultAccessor]); + controlDir = new NgFormControl([Validators.required], [defaultAccessor]); controlDir.valueAccessor = new DummyControlValueAccessor(); control = new Control(null); @@ -353,8 +371,6 @@ export function main() { }); it("should set up validator", () => { - controlDir.validators = [Validators.required]; - expect(control.valid).toBe(true); // this will add the required validator and recalculate the validity @@ -368,7 +384,7 @@ export function main() { var ngModel; beforeEach(() => { - ngModel = new NgModel([], [defaultAccessor]); + ngModel = new NgModel([Validators.required], [defaultAccessor]); ngModel.valueAccessor = new DummyControlValueAccessor(); }); @@ -385,8 +401,6 @@ export function main() { }); it("should set up validator", () => { - ngModel.validators = [Validators.required]; - expect(ngModel.control.valid).toBe(true); // this will add the required validator and recalculate the validity diff --git a/modules/angular2/test/core/forms/integration_spec.ts b/modules/angular2/test/core/forms/integration_spec.ts index 36a6555310..26505bf479 100644 --- a/modules/angular2/test/core/forms/integration_spec.ts +++ b/modules/angular2/test/core/forms/integration_spec.ts @@ -31,10 +31,13 @@ import { NgFor, NgForm, Validators, + forwardRef, + Validator } from 'angular2/core'; import {By} from 'angular2/src/core/debug'; import {ListWrapper} from 'angular2/src/core/facade/collection'; import {ObservableWrapper} from 'angular2/src/core/facade/async'; +import {CONST_EXPR} from 'angular2/src/core/facade/lang'; export function main() { describe("integration tests", () => { diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 0f628ab78c..68be0f8756 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -414,11 +414,10 @@ var NG_API = [ 'DecimalPipe.transform()', 'RequiredValidator', 'MinLengthValidator', - 'MinLengthValidator.minLength', - 'MinLengthValidator.minLength=', + 'MinLengthValidator.validate()', 'MaxLengthValidator', - 'MaxLengthValidator.maxLength', - 'MaxLengthValidator.maxLength=', + 'MaxLengthValidator.validate()', + 'Validator:dart', 'DefaultValueAccessor', 'DefaultValueAccessor.onChange', 'DefaultValueAccessor.onChange=', @@ -686,8 +685,6 @@ var NG_API = [ 'NgControlName.update=', 'NgControlName.valid', 'NgControlName.validator', - 'NgControlName.validators', - 'NgControlName.validators=', 'NgControlName.value', 'NgControlName.valueAccessor', 'NgControlName.valueAccessor=', @@ -745,8 +742,6 @@ var NG_API = [ 'NgFormControl.update=', 'NgFormControl.valid', 'NgFormControl.validator', - 'NgFormControl.validators', - 'NgFormControl.validators=', 'NgFormControl.value', 'NgFormControl.valueAccessor', 'NgFormControl.valueAccessor=', @@ -802,8 +797,6 @@ var NG_API = [ 'NgModel.update=', 'NgModel.valid', 'NgModel.validator', - 'NgModel.validators', - 'NgModel.validators=', 'NgModel.value', 'NgModel.valueAccessor', 'NgModel.valueAccessor=',