diff --git a/modules/angular2/src/common/forms/model.ts b/modules/angular2/src/common/forms/model.ts index 0ce36ddd884b..0a3fe951a024 100644 --- a/modules/angular2/src/common/forms/model.ts +++ b/modules/angular2/src/common/forms/model.ts @@ -1,5 +1,6 @@ import {StringWrapper, isPresent, isBlank, normalizeBool} from 'angular2/src/core/facade/lang'; import {Observable, EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async'; +import {PromiseWrapper} from 'angular2/src/core/facade/promise'; import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection'; /** @@ -42,6 +43,10 @@ function _find(control: AbstractControl, path: Array| string) { }, control); } +function toObservable(r: any): Observable { + return PromiseWrapper.isPromise(r) ? ObservableWrapper.fromPromise(r) : r; +} + /** * */ @@ -49,9 +54,8 @@ export abstract class AbstractControl { /** @internal */ _value: any; - /** @internal */ - _valueChanges: EventEmitter; - + private _valueChanges: EventEmitter; + private _statusChanges: EventEmitter; private _status: string; private _errors: {[key: string]: any}; private _controlsErrors: any; @@ -88,6 +92,8 @@ export abstract class AbstractControl { get valueChanges(): Observable { return this._valueChanges; } + get statusChanges(): Observable { return this._statusChanges; } + get pending(): boolean { return this._status == PENDING; } markAsTouched(): void { this._touched = true; } @@ -124,11 +130,12 @@ export abstract class AbstractControl { this._status = this._calculateStatus(); if (this._status == VALID || this._status == PENDING) { - this._runAsyncValidator(); + this._runAsyncValidator(emitEvent); } if (emitEvent) { ObservableWrapper.callNext(this._valueChanges, this._value); + ObservableWrapper.callNext(this._statusChanges, this._status); } if (isPresent(this._parent) && !onlySelf) { @@ -138,13 +145,13 @@ export abstract class AbstractControl { private _runValidator() { return isPresent(this.validator) ? this.validator(this) : null; } - private _runAsyncValidator() { + private _runAsyncValidator(emitEvent: boolean): void { if (isPresent(this.asyncValidator)) { this._status = PENDING; this._cancelExistingSubscription(); - var obs = ObservableWrapper.fromPromise(this.asyncValidator(this)); + var obs = toObservable(this.asyncValidator(this)); this._asyncValidationSubscription = - ObservableWrapper.subscribe(obs, res => this.setErrors(res)); + ObservableWrapper.subscribe(obs, res => this.setErrors(res, {emitEvent: emitEvent})); } } @@ -177,10 +184,16 @@ export abstract class AbstractControl { * expect(login.valid).toEqual(true); * ``` */ - setErrors(errors: {[key: string]: any}): void { + setErrors(errors: {[key: string]: any}, {emitEvent}: {emitEvent?: boolean} = {}): void { + emitEvent = isPresent(emitEvent) ? emitEvent : true; + this._errors = errors; this._status = this._calculateStatus(); + if (emitEvent) { + ObservableWrapper.callNext(this._statusChanges, this._status); + } + if (isPresent(this._parent)) { this._parent._updateControlsErrors(); } @@ -211,6 +224,13 @@ export abstract class AbstractControl { } } + /** @internal */ + _initObservables() { + this._valueChanges = new EventEmitter(); + this._statusChanges = new EventEmitter(); + } + + private _calculateStatus(): string { if (isPresent(this._errors)) return INVALID; if (this._anyControlsHaveStatus(PENDING)) return PENDING; @@ -250,7 +270,7 @@ export class Control extends AbstractControl { super(validator, asyncValidator); this._value = value; this.updateValueAndValidity({onlySelf: true, emitEvent: false}); - this._valueChanges = new EventEmitter(); + this._initObservables(); } /** @@ -318,8 +338,7 @@ export class ControlGroup extends AbstractControl { asyncValidator: Function = null) { super(validator, asyncValidator); this._optionals = isPresent(optionals) ? optionals : {}; - this._valueChanges = new EventEmitter(); - + this._initObservables(); this._setParentForControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); } @@ -440,9 +459,7 @@ export class ControlArray extends AbstractControl { constructor(public controls: AbstractControl[], validator: Function = null, asyncValidator: Function = null) { super(validator, asyncValidator); - - this._valueChanges = new EventEmitter(); - + this._initObservables(); this._setParentForControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); } diff --git a/modules/angular2/src/common/forms/validators.ts b/modules/angular2/src/common/forms/validators.ts index 85c53c45e5cf..32d09cc1366e 100644 --- a/modules/angular2/src/common/forms/validators.ts +++ b/modules/angular2/src/common/forms/validators.ts @@ -1,5 +1,6 @@ import {isBlank, isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang'; import {PromiseWrapper} from 'angular2/src/core/facade/promise'; +import {ObservableWrapper} from 'angular2/src/core/facade/async'; import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection'; import {OpaqueToken} from 'angular2/src/core/di'; @@ -89,15 +90,20 @@ export class Validators { static composeAsync(validators: Function[]): Function { if (isBlank(validators)) return null; - var presentValidators = ListWrapper.filter(validators, isPresent); + let presentValidators = ListWrapper.filter(validators, isPresent); if (presentValidators.length == 0) return null; return function(control: modelModule.AbstractControl) { - return PromiseWrapper.all(_executeValidators(control, presentValidators)).then(_mergeErrors); + let promises = _executeValidators(control, presentValidators).map(_convertToPromise); + return PromiseWrapper.all(promises).then(_mergeErrors); }; } } +function _convertToPromise(obj: any): any { + return PromiseWrapper.isPromise(obj) ? obj : ObservableWrapper.toPromise(obj); +} + function _executeValidators(control: modelModule.AbstractControl, validators: Function[]): any[] { return validators.map(v => v(control)); } diff --git a/modules/angular2/src/core/facade/async.dart b/modules/angular2/src/core/facade/async.dart index c454330e3584..6ffa49ab045f 100644 --- a/modules/angular2/src/core/facade/async.dart +++ b/modules/angular2/src/core/facade/async.dart @@ -61,6 +61,10 @@ class ObservableWrapper { static Stream fromPromise(Future f) { return new Stream.fromFuture(f); } + + static Future toPromise(Stream s) { + return s.single; + } } class EventEmitter extends Stream { diff --git a/modules/angular2/src/core/facade/async.ts b/modules/angular2/src/core/facade/async.ts index 33f039dfb3d9..249f30b19f36 100644 --- a/modules/angular2/src/core/facade/async.ts +++ b/modules/angular2/src/core/facade/async.ts @@ -48,6 +48,8 @@ export class ObservableWrapper { static fromPromise(promise: Promise): Observable { return RxObservable.fromPromise(promise); } + + static toPromise(obj: Observable): Promise { return (obj).toPromise(); } } /** diff --git a/modules/angular2/src/core/facade/promise.dart b/modules/angular2/src/core/facade/promise.dart index f8a6c3f61709..0970f37eb6f1 100644 --- a/modules/angular2/src/core/facade/promise.dart +++ b/modules/angular2/src/core/facade/promise.dart @@ -34,6 +34,10 @@ class PromiseWrapper { async.scheduleMicrotask(fn); } + static bool isPromise(obj) { + return obj is Future; + } + static PromiseCompleter completer() => new PromiseCompleter(new Completer()); } diff --git a/modules/angular2/src/core/facade/promise.ts b/modules/angular2/src/core/facade/promise.ts index 27bbc41a7fd9..eaeff6d351ae 100644 --- a/modules/angular2/src/core/facade/promise.ts +++ b/modules/angular2/src/core/facade/promise.ts @@ -44,6 +44,8 @@ export class PromiseWrapper { PromiseWrapper.then(PromiseWrapper.resolve(null), computation, (_) => {}); } + static isPromise(obj: any): boolean { return obj instanceof Promise; } + static completer(): PromiseCompleter { var resolve; var reject; diff --git a/modules/angular2/src/core/linker/view_pool.ts b/modules/angular2/src/core/linker/view_pool.ts index a92f75504938..b2a0bb10738b 100644 --- a/modules/angular2/src/core/linker/view_pool.ts +++ b/modules/angular2/src/core/linker/view_pool.ts @@ -1,7 +1,7 @@ import {Inject, Injectable, OpaqueToken} from 'angular2/src/core/di'; -import {MapWrapper, Map} from 'angular2/src/core/facade/collection'; import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang'; +import {MapWrapper, Map} from 'angular2/src/core/facade/collection'; import * as viewModule from './view'; diff --git a/modules/angular2/test/common/forms/model_spec.ts b/modules/angular2/test/common/forms/model_spec.ts index add4c878e6d6..3537d17f5b4e 100644 --- a/modules/angular2/test/common/forms/model_spec.ts +++ b/modules/angular2/test/common/forms/model_spec.ts @@ -14,10 +14,9 @@ import { inject } from 'angular2/testing_internal'; import {ControlGroup, Control, ControlArray, Validators} from 'angular2/core'; -import {isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang'; +import {IS_DART, isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang'; import {PromiseWrapper} from 'angular2/src/core/facade/promise'; -import {TimerWrapper, ObservableWrapper} from 'angular2/src/core/facade/async'; -import {IS_DART} from 'angular2/src/core/facade/lang'; +import {TimerWrapper, ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async'; export function main() { function asyncValidator(expected, timeouts = CONST_EXPR({})) { @@ -36,6 +35,12 @@ export function main() { }; } + function asyncValidatorReturningObservable(c) { + var e = new EventEmitter(); + PromiseWrapper.scheduleMicrotask(() => ObservableWrapper.callNext(e, {"async": true})); + return e; + } + describe("Form Model", () => { describe("Control", () => { it("should default the value to null", () => { @@ -70,6 +75,14 @@ export function main() { expect(c.errors).toEqual({"async": true}); })); + it("should support validators returning observables", fakeAsync(() => { + var c = new Control("value", null, asyncValidatorReturningObservable); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({"async": true}); + })); + it("should rerun the validator when the value changes", fakeAsync(() => { var c = new Control("value", null, asyncValidator("expected")); @@ -185,7 +198,7 @@ export function main() { })); }); - describe("valueChanges", () => { + describe("valueChanges & statusChanges", () => { var c; beforeEach(() => { c = new Control("old", Validators.required); }); @@ -200,6 +213,45 @@ export function main() { c.updateValue("new"); })); + it("should fire an event after the status has been updated to invalid", fakeAsync(() => { + ObservableWrapper.subscribe(c.statusChanges, (status) => { + expect(c.status).toEqual('INVALID'); + expect(status).toEqual('INVALID'); + }); + + c.updateValue(""); + tick(); + })); + + it("should fire an event after the status has been updated to pending", fakeAsync(() => { + var c = new Control("old", Validators.required, asyncValidator("expected")); + + var log = []; + ObservableWrapper.subscribe(c.valueChanges, (value) => log.push(`value: '${value}'`)); + ObservableWrapper.subscribe(c.statusChanges, + (status) => log.push(`status: '${status}'`)); + + c.updateValue(""); + tick(); + + c.updateValue("nonEmpty"); + tick(); + + c.updateValue("expected"); + tick(); + + expect(log).toEqual([ + "" + "value: ''", + "status: 'INVALID'", + "value: 'nonEmpty'", + "status: 'PENDING'", + "status: 'INVALID'", + "value: 'expected'", + "status: 'PENDING'", + "status: 'VALID'", + ]); + })); + // TODO: remove the if statement after making observable delivery sync if (!IS_DART) { it("should update set errors and status before emitting an event", diff --git a/modules/angular2/test/common/forms/validators_spec.ts b/modules/angular2/test/common/forms/validators_spec.ts index a4dcf0ad4232..d20903b18bc6 100644 --- a/modules/angular2/test/common/forms/validators_spec.ts +++ b/modules/angular2/test/common/forms/validators_spec.ts @@ -13,7 +13,7 @@ import { } 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 {EventEmitter, ObservableWrapper, TimerWrapper} from 'angular2/src/core/facade/async'; import {CONST_EXPR} from 'angular2/src/core/facade/lang'; export function main() { @@ -95,12 +95,19 @@ export function main() { }); describe("composeAsync", () => { - function asyncValidator(expected, response, timeout = 0) { + function asyncValidator(expected, response) { return (c) => { - var completer = PromiseWrapper.completer(); + var emitter = new EventEmitter(); var res = c.value != expected ? response : null; - TimerWrapper.setTimeout(() => { completer.resolve(res); }, timeout); - return completer.promise; + + PromiseWrapper.scheduleMicrotask(() => { + ObservableWrapper.callNext(emitter, res); + // this is required because of a bug in ObservableWrapper + // where callComplete can fire before callNext + // remove this one the bug is fixed + TimerWrapper.setTimeout(() => { ObservableWrapper.callComplete(emitter); }, 0); + }); + return emitter; }; } diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index ce29d3913909..120985110f20 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -65,6 +65,7 @@ var NG_ALL = [ 'AbstractControl.asyncValidator=', 'AbstractControl.value', 'AbstractControl.valueChanges', + 'AbstractControl.statusChanges', 'AbstractControlDirective', 'AbstractControlDirective.control', 'AbstractControlDirective.dirty', @@ -307,6 +308,7 @@ var NG_ALL = [ 'Control.asyncValidator=', 'Control.value', 'Control.valueChanges', + 'Control.statusChanges', 'Control.setErrors()', 'ControlArray', 'ControlArray.at()', @@ -339,6 +341,7 @@ var NG_ALL = [ 'ControlArray.asyncValidator=', 'ControlArray.value', 'ControlArray.valueChanges', + 'ControlArray.statusChanges', 'ControlArray.setErrors()', 'ControlContainer', 'ControlContainer.control', @@ -385,6 +388,7 @@ var NG_ALL = [ 'ControlGroup.asyncValidator=', 'ControlGroup.value', 'ControlGroup.valueChanges', + 'ControlGroup.statusChanges', 'ControlGroup.setErrors()', 'CurrencyPipe', 'CurrencyPipe.transform()',