From cf449ddaa969a7d1aac4e1a6a93e247bc9b62998 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 2 Nov 2015 09:59:40 -0800 Subject: [PATCH] feat(forms): implements a combinator for composing async validators --- modules/angular2/src/core/forms/validators.ts | 33 +++++++++-- .../test/core/forms/validators_spec.ts | 59 ++++++++++++++++++- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/modules/angular2/src/core/forms/validators.ts b/modules/angular2/src/core/forms/validators.ts index d96ec40e5b..8f6b2735ba 100644 --- a/modules/angular2/src/core/forms/validators.ts +++ b/modules/angular2/src/core/forms/validators.ts @@ -3,6 +3,7 @@ import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection import {OpaqueToken} from 'angular2/src/core/di'; import * as modelModule from './model'; +import {PromiseWrapper} from "../facade/promise"; /** * Providers for validators to be used for {@link Control}s in a form. @@ -18,6 +19,7 @@ import * as modelModule from './model'; * ``` */ export const NG_VALIDATORS: OpaqueToken = CONST_EXPR(new OpaqueToken("NgValidators")); +export const NG_ASYNC_VALIDATORS: OpaqueToken = CONST_EXPR(new OpaqueToken("NgAsyncValidators")); /** * Provides a set of validators used by form controls. @@ -76,14 +78,33 @@ export class Validators { * of the individual error maps. */ static compose(validators: Function[]): Function { - if (isBlank(validators)) return Validators.nullValidator; + if (isBlank(validators)) return null; + var presentValidators = ListWrapper.filter(validators, isPresent); + if (presentValidators.length == 0) return null; return function(control: modelModule.AbstractControl) { - var res = ListWrapper.reduce(validators, (res, validator) => { - var errors = isPresent(validator) ? validator(control) : null; - return isPresent(errors) ? StringMapWrapper.merge(res, errors) : res; - }, {}); - return StringMapWrapper.isEmpty(res) ? null : res; + return _mergeErrors(_executeValidators(control, presentValidators)); + }; + } + + static composeAsync(validators: Function[]): Function { + if (isBlank(validators)) return null; + var presentValidators = ListWrapper.filter(validators, isPresent); + if (presentValidators.length == 0) return null; + + return function(control: modelModule.AbstractControl) { + return PromiseWrapper.all(_executeValidators(control, presentValidators)).then(_mergeErrors); }; } } + +function _executeValidators(control: modelModule.AbstractControl, validators: Function[]): any[] { + return validators.map(v => v(control)); +} + +function _mergeErrors(arrayOfErrors: any[]): {[key: string]: any} { + var res = ListWrapper.reduce(arrayOfErrors, (res, errors) => { + return isPresent(errors) ? StringMapWrapper.merge(res, errors) : res; + }, {}); + return StringMapWrapper.isEmpty(res) ? null : res; +} diff --git a/modules/angular2/test/core/forms/validators_spec.ts b/modules/angular2/test/core/forms/validators_spec.ts index 9182fc2ef8..a4dcf0ad42 100644 --- a/modules/angular2/test/core/forms/validators_spec.ts +++ b/modules/angular2/test/core/forms/validators_spec.ts @@ -7,9 +7,14 @@ import { expect, beforeEach, afterEach, + fakeAsync, + tick, el } from 'angular2/testing_internal'; import {ControlGroup, Control, Validators, AbstractControl, ControlArray} from 'angular2/core'; +import {PromiseWrapper} from 'angular2/src/core/facade/promise'; +import {TimerWrapper} from 'angular2/src/core/facade/async'; +import {CONST_EXPR} from 'angular2/src/core/facade/lang'; export function main() { function validator(key: string, error: any) { @@ -65,8 +70,8 @@ export function main() { }); describe("compose", () => { - it("should return a null validator when given null", - () => { expect(Validators.compose(null)).toBe(Validators.nullValidator); }); + it("should return null when given null", + () => { expect(Validators.compose(null)).toBe(null); }); it("should collect errors from all the validators", () => { var c = Validators.compose([validator("a", true), validator("b", true)]); @@ -88,5 +93,55 @@ export function main() { expect(c(new Control(""))).toEqual({"required": true}); }); }); + + describe("composeAsync", () => { + function asyncValidator(expected, response, timeout = 0) { + return (c) => { + var completer = PromiseWrapper.completer(); + var res = c.value != expected ? response : null; + TimerWrapper.setTimeout(() => { completer.resolve(res); }, timeout); + return completer.promise; + }; + } + + it("should return null when given null", + () => { expect(Validators.composeAsync(null)).toEqual(null); }); + + it("should collect errors from all the validators", fakeAsync(() => { + var c = Validators.composeAsync([ + asyncValidator("expected", {"one": true}), + asyncValidator("expected", {"two": true}) + ]); + + var value = null; + c(new Control("invalid")).then(v => value = v); + + tick(1); + + expect(value).toEqual({"one": true, "two": true}); + })); + + it("should return null when no errors", fakeAsync(() => { + var c = Validators.composeAsync([asyncValidator("expected", {"one": true})]); + + var value = null; + c(new Control("expected")).then(v => value = v); + + tick(1); + + expect(value).toEqual(null); + })); + + it("should ignore nulls", fakeAsync(() => { + var c = Validators.composeAsync([asyncValidator("expected", {"one": true}), null]); + + var value = null; + c(new Control("invalid")).then(v => value = v); + + tick(1); + + expect(value).toEqual({"one": true}); + })); + }); }); }