From 31c12af81facc0621356df9edd693d06bec606fd Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 10:00:42 -0800 Subject: [PATCH] feat(forms): add support for adding async validators via template Example: @Directive({ selector: '[uniq-login-validator]', providers: [provide(NG_ASYNC_VALIDATORS, {useExisting: UniqLoginValidator, multi: true})] }) class UniqLoginValidator implements Validator { validate(c) { return someFunctionReturningPromiseOrObservable(); } } --- modules/angular2/src/core/forms.ts | 2 +- .../src/core/forms/directives/ng_control.ts | 1 + .../core/forms/directives/ng_control_group.ts | 14 +- .../core/forms/directives/ng_control_name.ts | 27 ++-- .../src/core/forms/directives/ng_form.ts | 10 +- .../core/forms/directives/ng_form_control.ts | 20 ++- .../core/forms/directives/ng_form_model.ts | 18 ++- .../src/core/forms/directives/ng_model.ts | 20 ++- .../src/core/forms/directives/shared.ts | 10 +- modules/angular2/src/core/forms/model.ts | 3 +- .../test/core/forms/directives_spec.ts | 143 ++++++++++++------ .../test/core/forms/integration_spec.ts | 62 +++++++- .../angular2/test/core/forms/model_spec.ts | 13 +- modules/angular2/test/public_api_spec.ts | 7 + 14 files changed, 249 insertions(+), 101 deletions(-) diff --git a/modules/angular2/src/core/forms.ts b/modules/angular2/src/core/forms.ts index 7cb45517ab..8e82ce44b0 100644 --- a/modules/angular2/src/core/forms.ts +++ b/modules/angular2/src/core/forms.ts @@ -33,7 +33,7 @@ export { SelectControlValueAccessor } from './forms/directives/select_control_value_accessor'; export {FORM_DIRECTIVES} from './forms/directives'; -export {NG_VALIDATORS, Validators} from './forms/validators'; +export {NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validators} from './forms/validators'; export { RequiredValidator, MinLengthValidator, diff --git a/modules/angular2/src/core/forms/directives/ng_control.ts b/modules/angular2/src/core/forms/directives/ng_control.ts index 8ec16089a0..1b68f4c38e 100644 --- a/modules/angular2/src/core/forms/directives/ng_control.ts +++ b/modules/angular2/src/core/forms/directives/ng_control.ts @@ -13,6 +13,7 @@ export abstract class NgControl extends AbstractControlDirective { valueAccessor: ControlValueAccessor = null; get validator(): Function { return unimplemented(); } + get asyncValidator(): Function { return unimplemented(); } abstract viewToModelUpdate(newValue: any): void; } diff --git a/modules/angular2/src/core/forms/directives/ng_control_group.ts b/modules/angular2/src/core/forms/directives/ng_control_group.ts index 94ae2876ee..310c38a8a1 100644 --- a/modules/angular2/src/core/forms/directives/ng_control_group.ts +++ b/modules/angular2/src/core/forms/directives/ng_control_group.ts @@ -5,10 +5,10 @@ import {ListWrapper} from 'angular2/src/core/facade/collection'; import {CONST_EXPR} from 'angular2/src/core/facade/lang'; import {ControlContainer} from './control_container'; -import {controlPath} from './shared'; +import {controlPath, composeValidators, composeAsyncValidators} from './shared'; import {ControlGroup} from '../model'; import {Form} from './form_interface'; -import {Validators, NG_VALIDATORS} from '../validators'; +import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; const controlGroupProvider = CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgControlGroup)})); @@ -72,13 +72,11 @@ export class NgControlGroup extends ControlContainer implements OnInit, /** @internal */ _parent: ControlContainer; - private _validators: Function[]; - constructor(@Host() @SkipSelf() parent: ControlContainer, - @Optional() @Inject(NG_VALIDATORS) validators: Function[]) { + @Optional() @Inject(NG_VALIDATORS) private _validators: any[], + @Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) { super(); this._parent = parent; - this._validators = validators; } onInit(): void { this.formDirective.addControlGroup(this); } @@ -100,5 +98,7 @@ export class NgControlGroup extends ControlContainer implements OnInit, */ get formDirective(): Form { return this._parent.formDirective; } - get validator(): Function { return Validators.compose(this._validators); } + get validator(): Function { return composeValidators(this._validators); } + + get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); } } 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 c8f3942396..a36522b5dc 100644 --- a/modules/angular2/src/core/forms/directives/ng_control_name.ts +++ b/modules/angular2/src/core/forms/directives/ng_control_name.ts @@ -8,9 +8,15 @@ 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, composeValidators, isPropertyUpdated, selectValueAccessor} from './shared'; +import { + controlPath, + composeValidators, + composeAsyncValidators, + isPropertyUpdated, + selectValueAccessor +} from './shared'; import {Control} from '../model'; -import {Validators, NG_VALIDATORS} from '../validators'; +import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; const controlNameBinding = @@ -81,21 +87,18 @@ const controlNameBinding = export class NgControlName extends NgControl implements OnChanges, OnDestroy { /** @internal */ - _parent: ControlContainer; update = new EventEmitter(); model: any; viewModel: any; - private _validator: Function; - /** @internal */ - _added = false; + private _added = false; - constructor(@Host() @SkipSelf() parent: ControlContainer, - @Optional() @Inject(NG_VALIDATORS) validators: + constructor(@Host() @SkipSelf() private _parent: ControlContainer, + @Optional() @Inject(NG_VALIDATORS) private _validators: + /* Array */ any[], + @Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: /* Array */ any[], @Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { super(); - this._parent = parent; - this._validator = composeValidators(validators); this.valueAccessor = selectValueAccessor(this, valueAccessors); } @@ -121,7 +124,9 @@ export class NgControlName extends NgControl implements OnChanges, get formDirective(): any { return this._parent.formDirective; } - get validator(): Function { return this._validator; } + get validator(): Function { return composeValidators(this._validators); } + + get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); } get control(): Control { return this.formDirective.getControl(this); } } diff --git a/modules/angular2/src/core/forms/directives/ng_form.ts b/modules/angular2/src/core/forms/directives/ng_form.ts index 3c23e3d7d4..937697356a 100644 --- a/modules/angular2/src/core/forms/directives/ng_form.ts +++ b/modules/angular2/src/core/forms/directives/ng_form.ts @@ -13,8 +13,8 @@ import {Form} from './form_interface'; import {NgControlGroup} from './ng_control_group'; import {ControlContainer} from './control_container'; import {AbstractControl, ControlGroup, Control} from '../model'; -import {setUpControl, setUpControlGroup} from './shared'; -import {Validators, NG_VALIDATORS} from '../validators'; +import {setUpControl, setUpControlGroup, composeValidators, composeAsyncValidators} from './shared'; +import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; const formDirectiveProvider = CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgForm)})); @@ -91,9 +91,11 @@ export class NgForm extends ControlContainer implements Form { form: ControlGroup; ngSubmit = new EventEmitter(); - constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) { + constructor(@Optional() @Inject(NG_VALIDATORS) validators: any[], + @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { super(); - this.form = new ControlGroup({}, null, Validators.compose(validators)); + this.form = new ControlGroup({}, null, composeValidators(validators), + composeAsyncValidators(asyncValidators)); } get formDirective(): Form { return 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 d7a809cfc5..4de8844061 100644 --- a/modules/angular2/src/core/forms/directives/ng_form_control.ts +++ b/modules/angular2/src/core/forms/directives/ng_form_control.ts @@ -7,9 +7,15 @@ import {Query, Directive} from 'angular2/src/core/metadata'; import {forwardRef, Provider, Inject, Optional} from 'angular2/src/core/di'; import {NgControl} from './ng_control'; import {Control} from '../model'; -import {Validators, NG_VALIDATORS} from '../validators'; +import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; -import {setUpControl, composeValidators, isPropertyUpdated, selectValueAccessor} from './shared'; +import { + setUpControl, + composeValidators, + composeAsyncValidators, + isPropertyUpdated, + selectValueAccessor +} from './shared'; const formControlBinding = CONST_EXPR(new Provider(NgControl, {useExisting: forwardRef(() => NgFormControl)})); @@ -73,13 +79,13 @@ export class NgFormControl extends NgControl implements OnChanges { update = new EventEmitter(); model: any; viewModel: any; - private _validator: Function; - constructor(@Optional() @Inject(NG_VALIDATORS) validators: + constructor(@Optional() @Inject(NG_VALIDATORS) private _validators: + /* Array */ any[], + @Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: /* Array */ any[], @Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { super(); - this._validator = composeValidators(validators); this.valueAccessor = selectValueAccessor(this, valueAccessors); } @@ -96,7 +102,9 @@ export class NgFormControl extends NgControl implements OnChanges { get path(): string[] { return []; } - get validator(): Function { return this._validator; } + get validator(): Function { return composeValidators(this._validators); } + + get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); } get control(): Control { return this.form; } diff --git a/modules/angular2/src/core/forms/directives/ng_form_model.ts b/modules/angular2/src/core/forms/directives/ng_form_model.ts index ee376810f5..45e9a141d9 100644 --- a/modules/angular2/src/core/forms/directives/ng_form_model.ts +++ b/modules/angular2/src/core/forms/directives/ng_form_model.ts @@ -11,8 +11,8 @@ import {NgControlGroup} from './ng_control_group'; import {ControlContainer} from './control_container'; import {Form} from './form_interface'; import {Control, ControlGroup} from '../model'; -import {setUpControl, setUpControlGroup} from './shared'; -import {Validators, NG_VALIDATORS} from '../validators'; +import {setUpControl, setUpControlGroup, composeValidators, composeAsyncValidators} from './shared'; +import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; const formDirectiveProvider = CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgFormModel)})); @@ -102,17 +102,21 @@ export class NgFormModel extends ControlContainer implements Form, form: ControlGroup = null; directives: NgControl[] = []; ngSubmit = new EventEmitter(); - private _validators: Function[]; - constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) { + constructor(@Optional() @Inject(NG_VALIDATORS) private _validators: any[], + @Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) { super(); - this._validators = validators; } onChanges(changes: {[key: string]: SimpleChange}): void { if (StringMapWrapper.contains(changes, "form")) { - var c = Validators.compose(this._validators); - this.form.validator = Validators.compose([this.form.validator, c]); + var sync = composeValidators(this._validators); + this.form.validator = Validators.compose([this.form.validator, sync]); + + var async = composeAsyncValidators(this._asyncValidators); + this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]); + + this.form.updateValueAndValidity({onlySelf: true, emitEvent: false}); } this._updateDomValue(); diff --git a/modules/angular2/src/core/forms/directives/ng_model.ts b/modules/angular2/src/core/forms/directives/ng_model.ts index 50ce974c98..b1471fc0be 100644 --- a/modules/angular2/src/core/forms/directives/ng_model.ts +++ b/modules/angular2/src/core/forms/directives/ng_model.ts @@ -7,8 +7,14 @@ import {forwardRef, Provider, Inject, Optional} from 'angular2/src/core/di'; 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, composeValidators} from './shared'; +import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; +import { + setUpControl, + isPropertyUpdated, + selectValueAccessor, + composeValidators, + composeAsyncValidators +} from './shared'; const formControlBinding = CONST_EXPR(new Provider(NgControl, {useExisting: forwardRef(() => NgModel)})); @@ -49,13 +55,11 @@ export class NgModel extends NgControl implements OnChanges { update = new EventEmitter(); model: any; viewModel: any; - private _validator: Function; - constructor(@Optional() @Inject(NG_VALIDATORS) validators: - /* Array */ any[], + constructor(@Optional() @Inject(NG_VALIDATORS) private _validators: any[], + @Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[], @Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { super(); - this._validator = composeValidators(validators); this.valueAccessor = selectValueAccessor(this, valueAccessors); } @@ -76,7 +80,9 @@ export class NgModel extends NgControl implements OnChanges { get path(): string[] { return []; } - get validator(): Function { return this._validator; } + get validator(): Function { return composeValidators(this._validators); } + + get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); } viewToModelUpdate(newValue: any): void { this.viewModel = newValue; diff --git a/modules/angular2/src/core/forms/directives/shared.ts b/modules/angular2/src/core/forms/directives/shared.ts index b11ba63d3f..0dbd7edf5d 100644 --- a/modules/angular2/src/core/forms/directives/shared.ts +++ b/modules/angular2/src/core/forms/directives/shared.ts @@ -29,6 +29,7 @@ export function setUpControl(control: Control, dir: NgControl): void { if (isBlank(dir.valueAccessor)) _throwError(dir, "No value accessor for"); control.validator = Validators.compose([control.validator, dir.validator]); + control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]); dir.valueAccessor.writeValue(control.value); // view -> model @@ -48,6 +49,7 @@ export function setUpControl(control: Control, dir: NgControl): void { export function setUpControlGroup(control: ControlGroup, dir: NgControlGroup) { if (isBlank(control)) _throwError(dir, "Cannot find control"); control.validator = Validators.compose([control.validator, dir.validator]); + control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]); } function _throwError(dir: AbstractControlDirective, message: string): void { @@ -61,8 +63,12 @@ export function setProperty(renderer: Renderer, elementRef: ElementRef, propName } export function composeValidators(validators: /* Array */ any[]): Function { - return isPresent(validators) ? Validators.compose(validators.map(normalizeValidator)) : - Validators.nullValidator; + return isPresent(validators) ? Validators.compose(validators.map(normalizeValidator)) : null; +} + +export function composeAsyncValidators( + validators: /* Array */ any[]): Function { + return isPresent(validators) ? Validators.composeAsync(validators.map(normalizeValidator)) : null; } export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any): boolean { diff --git a/modules/angular2/src/core/forms/model.ts b/modules/angular2/src/core/forms/model.ts index 5b633785d9..0ce36ddd88 100644 --- a/modules/angular2/src/core/forms/model.ts +++ b/modules/angular2/src/core/forms/model.ts @@ -142,8 +142,9 @@ export abstract class AbstractControl { if (isPresent(this.asyncValidator)) { this._status = PENDING; this._cancelExistingSubscription(); + var obs = ObservableWrapper.fromPromise(this.asyncValidator(this)); this._asyncValidationSubscription = - ObservableWrapper.subscribe(this.asyncValidator(this), res => this.setErrors(res)); + ObservableWrapper.subscribe(obs, res => this.setErrors(res)); } } diff --git a/modules/angular2/test/core/forms/directives_spec.ts b/modules/angular2/test/core/forms/directives_spec.ts index d7212c6f3b..f585699325 100644 --- a/modules/angular2/test/core/forms/directives_spec.ts +++ b/modules/angular2/test/core/forms/directives_spec.ts @@ -11,7 +11,8 @@ import { afterEach, el, AsyncTestCompleter, - inject + inject, + tick } from 'angular2/testing_internal'; import {SpyNgControl, SpyValueAccessor} from '../spies'; @@ -38,7 +39,8 @@ import { import {selectValueAccessor, composeValidators} from 'angular2/src/core/forms/directives/shared'; - +import {TimerWrapper} from 'angular2/src/core/facade/async'; +import {PromiseWrapper} from 'angular2/src/core/facade/promise'; import {SimpleChange} from 'angular2/src/core/change_detection'; class DummyControlValueAccessor implements ControlValueAccessor { @@ -54,6 +56,19 @@ class CustomValidatorDirective implements Validator { validate(c: Control): {[key: string]: any} { return {"custom": true}; } } +function asyncValidator(expected, timeout = 0) { + return (c) => { + var completer = PromiseWrapper.completer(); + var res = c.value != expected ? {"async": true} : null; + if (timeout == 0) { + completer.resolve(res); + } else { + TimerWrapper.setTimeout(() => { completer.resolve(res); }, timeout); + } + return completer.promise; + }; +} + export function main() { describe("Form Directives", () => { var defaultAccessor; @@ -125,7 +140,7 @@ export function main() { var loginControlDir; beforeEach(() => { - form = new NgFormModel([]); + form = new NgFormModel([], []); formModel = new ControlGroup({ "login": new Control(), "passwords": @@ -133,7 +148,8 @@ export function main() { }); form.form = formModel; - loginControlDir = new NgControlName(form, [Validators.required], [defaultAccessor]); + loginControlDir = new NgControlName(form, [Validators.required], + [asyncValidator("expected")], [defaultAccessor]); loginControlDir.name = "login"; loginControlDir.valueAccessor = new DummyControlValueAccessor(); }); @@ -151,7 +167,7 @@ export function main() { describe("addControl", () => { it("should throw when no control found", () => { - var dir = new NgControlName(form, null, [defaultAccessor]); + var dir = new NgControlName(form, null, null, [defaultAccessor]); dir.name = "invalidName"; expect(() => form.addControl(dir)) @@ -159,21 +175,30 @@ export function main() { }); it("should throw when no value accessor", () => { - var dir = new NgControlName(form, null, null); + var dir = new NgControlName(form, null, null, null); dir.name = "login"; expect(() => form.addControl(dir)) .toThrowError(new RegExp("No value accessor for 'login'")); }); - it("should set up validator", () => { - expect(formModel.find(["login"]).valid).toBe(true); + it("should set up validators", fakeAsync(() => { + form.addControl(loginControlDir); - // this will add the required validator and recalculate the validity - form.addControl(loginControlDir); + // sync validators are set + expect(formModel.hasError("required", ["login"])).toBe(true); + expect(formModel.hasError("async", ["login"])).toBe(false); - expect(formModel.find(["login"]).valid).toBe(false); - }); + formModel.find(["login"]).updateValue("invalid value"); + + // sync validator passes, running async validators + expect(formModel.pending).toBe(true); + + tick(); + + expect(formModel.hasError("required", ["login"])).toBe(false); + expect(formModel.hasError("async", ["login"])).toBe(true); + })); it("should write value to the DOM", () => { formModel.find(["login"]).updateValue("initValue"); @@ -198,15 +223,27 @@ export function main() { } }; - it("should set up validator", () => { - var group = new NgControlGroup(form, [matchingPasswordsValidator]); - group.name = "passwords"; - form.addControlGroup(group); + it("should set up validator", fakeAsync(() => { + var group = new NgControlGroup(form, [matchingPasswordsValidator], + [asyncValidator('expected')]); + group.name = "passwords"; + form.addControlGroup(group); - formModel.find(["passwords", "password"]).updateValue("somePassword"); + formModel.find(["passwords", "password"]).updateValue("somePassword"); + formModel.find(["passwords", "passwordConfirm"]).updateValue("someOtherPassword"); - expect(formModel.hasError("differentPasswords", ["passwords"])).toEqual(true); - }); + // sync validators are set + expect(formModel.hasError("differentPasswords", ["passwords"])).toEqual(true); + + formModel.find(["passwords", "passwordConfirm"]).updateValue("somePassword"); + + // sync validators pass, running async validators + expect(formModel.pending).toBe(true); + + tick(); + + expect(formModel.hasError("async", ["passwords"])).toBe(true); + })); }); describe("removeControl", () => { @@ -228,17 +265,24 @@ export function main() { expect((loginControlDir.valueAccessor).writtenValue).toEqual("new value"); }); - it("should set up validator", () => { + it("should set up a sync validator", () => { var formValidator = (c) => ({"custom": true}); - var f = new NgFormModel([formValidator]); + var f = new NgFormModel([formValidator], []); f.form = formModel; f.onChanges({"form": formModel}); - // trigger validation - formModel.controls["login"].updateValue(""); - expect(formModel.errors).toEqual({"custom": true}); }); + + it("should set up an async validator", fakeAsync(() => { + var f = new NgFormModel([], [asyncValidator("expected")]); + f.form = formModel; + f.onChanges({"form": formModel}); + + tick(); + + expect(formModel.errors).toEqual({"async": true}); + })); }); }); @@ -249,13 +293,13 @@ export function main() { var personControlGroupDir; beforeEach(() => { - form = new NgForm([]); + form = new NgForm([], []); formModel = form.form; - personControlGroupDir = new NgControlGroup(form, []); + personControlGroupDir = new NgControlGroup(form, [], []); personControlGroupDir.name = "person"; - loginControlDir = new NgControlName(personControlGroupDir, null, [defaultAccessor]); + loginControlDir = new NgControlName(personControlGroupDir, null, null, [defaultAccessor]); loginControlDir.name = "login"; loginControlDir.valueAccessor = new DummyControlValueAccessor(); }); @@ -301,16 +345,22 @@ export function main() { // should update the form's value and validity }); - it("should set up validator", fakeAsync(() => { + it("should set up sync validator", fakeAsync(() => { var formValidator = (c) => ({"custom": true}); - var f = new NgForm([formValidator]); - f.addControlGroup(personControlGroupDir); - f.addControl(loginControlDir); + var f = new NgForm([formValidator], []); - flushMicrotasks(); + tick(); expect(f.form.errors).toEqual({"custom": true}); })); + + it("should set up async validator", fakeAsync(() => { + var f = new NgForm([], [asyncValidator("expected")]); + + tick(); + + expect(f.form.errors).toEqual({"async": true}); + })); }); describe("NgControlGroup", () => { @@ -320,9 +370,9 @@ export function main() { beforeEach(() => { formModel = new ControlGroup({"login": new Control(null)}); - var parent = new NgFormModel([]); + var parent = new NgFormModel([], []); parent.form = new ControlGroup({"group": formModel}); - controlGroupDir = new NgControlGroup(parent, []); + controlGroupDir = new NgControlGroup(parent, [], []); controlGroupDir.name = "group"; }); @@ -353,7 +403,7 @@ export function main() { }; beforeEach(() => { - controlDir = new NgFormControl([Validators.required], [defaultAccessor]); + controlDir = new NgFormControl([Validators.required], [], [defaultAccessor]); controlDir.valueAccessor = new DummyControlValueAccessor(); control = new Control(null); @@ -384,7 +434,8 @@ export function main() { var ngModel; beforeEach(() => { - ngModel = new NgModel([Validators.required], [defaultAccessor]); + ngModel = + new NgModel([Validators.required], [asyncValidator("expected")], [defaultAccessor]); ngModel.valueAccessor = new DummyControlValueAccessor(); }); @@ -400,14 +451,18 @@ export function main() { expect(ngModel.untouched).toBe(control.untouched); }); - it("should set up validator", () => { - expect(ngModel.control.valid).toBe(true); + it("should set up validator", fakeAsync(() => { + // this will add the required validator and recalculate the validity + ngModel.onChanges({}); + tick(); - // this will add the required validator and recalculate the validity - ngModel.onChanges({}); + expect(ngModel.control.errors).toEqual({"required": true}); - expect(ngModel.control.valid).toBe(false); - }); + ngModel.control.updateValue("someValue"); + tick(); + + expect(ngModel.control.errors).toEqual({"async": true}); + })); }); describe("NgControlName", () => { @@ -417,9 +472,9 @@ export function main() { beforeEach(() => { formModel = new Control("name"); - var parent = new NgFormModel([]); + var parent = new NgFormModel([], []); parent.form = new ControlGroup({"name": formModel}); - controlNameDir = new NgControlName(parent, [], [defaultAccessor]); + controlNameDir = new NgControlName(parent, [], [], [defaultAccessor]); controlNameDir.name = "name"; }); diff --git a/modules/angular2/test/core/forms/integration_spec.ts b/modules/angular2/test/core/forms/integration_spec.ts index fa5efb136e..ee2bf07164 100644 --- a/modules/angular2/test/core/forms/integration_spec.ts +++ b/modules/angular2/test/core/forms/integration_spec.ts @@ -20,11 +20,13 @@ import { import {DOM} from 'angular2/src/core/dom/dom_adapter'; import { + Input, Control, ControlGroup, ControlValueAccessor, FORM_DIRECTIVES, NG_VALIDATORS, + NG_ASYNC_VALIDATORS, Provider, NgControl, NgIf, @@ -401,7 +403,7 @@ export function main() { }); describe("validations", () => { - it("should use validators defined in html", + it("should use sync validators defined in html", inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { var form = new ControlGroup( {"login": new Control(""), "min": new Control(""), "max": new Control("")}); @@ -446,6 +448,35 @@ export function main() { }); })); + it("should use async validators defined in the html", + inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => { + var form = new ControlGroup({"login": new Control("")}); + + var t = `
+ +
`; + + var rootTC; + tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((root) => rootTC = root); + tick(); + + rootTC.debugElement.componentInstance.form = form; + rootTC.detectChanges(); + + expect(form.pending).toEqual(true); + + tick(100); + + expect(form.hasError("uniqLogin", ["login"])).toEqual(true); + + var input = rootTC.debugElement.query(By.css("input")); + input.nativeElement.value = "expected"; + dispatchEvent(input.nativeElement, "change"); + tick(100); + + expect(form.valid).toEqual(true); + }))); + it("should use sync validators defined in the model", inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { var form = new ControlGroup({"login": new Control("aa", Validators.required)}); @@ -961,10 +992,10 @@ class MyInput implements ControlValueAccessor { function uniqLoginAsyncValidator(expectedValue: string) { return (c) => { - var e = new EventEmitter(); + var completer = PromiseWrapper.completer(); var res = (c.value == expectedValue) ? null : {"uniqLogin": true}; - PromiseWrapper.scheduleMicrotask(() => ObservableWrapper.callNext(e, res)); - return e; + completer.resolve(res); + return completer.promise; }; } @@ -979,10 +1010,31 @@ function loginIsEmptyGroupValidator(c: ControlGroup) { class LoginIsEmptyValidator { } +@Directive({ + selector: '[uniq-login-validator]', + providers: [ + new Provider(NG_ASYNC_VALIDATORS, + {useExisting: forwardRef(() => UniqLoginValidator), multi: true}) + ] +}) +class UniqLoginValidator implements Validator { + @Input('uniq-login-validator') expected; + + validate(c) { return uniqLoginAsyncValidator(this.expected)(c); } +} + @Component({ selector: "my-comp", template: '', - directives: [FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator] + directives: [ + FORM_DIRECTIVES, + WrappedValue, + MyInput, + NgIf, + NgFor, + LoginIsEmptyValidator, + UniqLoginValidator + ] }) class MyComp { form: any; diff --git a/modules/angular2/test/core/forms/model_spec.ts b/modules/angular2/test/core/forms/model_spec.ts index a070e8b4b4..500d84f814 100644 --- a/modules/angular2/test/core/forms/model_spec.ts +++ b/modules/angular2/test/core/forms/model_spec.ts @@ -15,23 +15,24 @@ import { } from 'angular2/testing_internal'; import {ControlGroup, Control, ControlArray, Validators} from 'angular2/core'; import {isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang'; -import {EventEmitter, TimerWrapper, ObservableWrapper} from 'angular2/src/core/facade/async'; +import {PromiseWrapper} from 'angular2/src/core/facade/promise'; +import {TimerWrapper, ObservableWrapper} from 'angular2/src/core/facade/async'; import {IS_DART} from '../../platform'; -import {PromiseWrapper} from "angular2/src/core/facade/promise"; export function main() { function asyncValidator(expected, timeouts = CONST_EXPR({})) { return (c) => { - var e = new EventEmitter(); + var completer = PromiseWrapper.completer(); var t = isPresent(timeouts[c.value]) ? timeouts[c.value] : 0; var res = c.value != expected ? {"async": true} : null; if (t == 0) { - PromiseWrapper.scheduleMicrotask(() => { ObservableWrapper.callNext(e, res); }); + completer.resolve(res); } else { - TimerWrapper.setTimeout(() => { ObservableWrapper.callNext(e, res); }, t); + TimerWrapper.setTimeout(() => { completer.resolve(res); }, t); } - return e; + + return completer.promise; }; } diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 453761fba9..3594e58494 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -819,6 +819,7 @@ var NG_ALL = [ 'LowerCasePipe', 'LowerCasePipe.transform()', 'NG_VALIDATORS', + 'NG_ASYNC_VALIDATORS', 'NgClass', 'NgClass.doCheck()', 'NgClass.initialClasses=', @@ -837,6 +838,7 @@ var NG_ALL = [ 'NgControl.untouched', 'NgControl.valid', 'NgControl.validator', + 'NgControl.asyncValidator', 'NgControl.value', 'NgControl.valueAccessor', 'NgControl.valueAccessor=', @@ -857,6 +859,7 @@ var NG_ALL = [ 'NgControlGroup.valid', 'NgControlGroup.value', 'NgControlGroup.validator', + 'NgControlGroup.asyncValidator', 'NgControlStatus', 'NgControlStatus.ngClassDirty', 'NgControlStatus.ngClassInvalid', @@ -884,6 +887,7 @@ var NG_ALL = [ 'NgControlName.update=', 'NgControlName.valid', 'NgControlName.validator', + 'NgControlName.asyncValidator', 'NgControlName.value', 'NgControlName.valueAccessor', 'NgControlName.valueAccessor=', @@ -941,6 +945,7 @@ var NG_ALL = [ 'NgFormControl.update=', 'NgFormControl.valid', 'NgFormControl.validator', + 'NgFormControl.asyncValidator', 'NgFormControl.value', 'NgFormControl.valueAccessor', 'NgFormControl.valueAccessor=', @@ -996,6 +1001,7 @@ var NG_ALL = [ 'NgModel.update=', 'NgModel.valid', 'NgModel.validator', + 'NgModel.asyncValidator', 'NgModel.value', 'NgModel.valueAccessor', 'NgModel.valueAccessor=', @@ -1223,6 +1229,7 @@ var NG_ALL = [ 'UrlResolver', 'UrlResolver.resolve()', 'Validators#compose()', + 'Validators#composeAsync()', 'Validators#nullValidator()', 'Validators#required()', 'Validators#minLength()',