X Tutup
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions modules/angular2/src/common/forms/model.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -42,16 +43,19 @@ function _find(control: AbstractControl, path: Array<string | number>| string) {
}, control);
}

function toObservable(r: any): Observable<any> {
return PromiseWrapper.isPromise(r) ? ObservableWrapper.fromPromise(r) : r;
}

/**
*
*/
export abstract class AbstractControl {
/** @internal */
_value: any;

/** @internal */
_valueChanges: EventEmitter<any>;

private _valueChanges: EventEmitter<any>;
private _statusChanges: EventEmitter<any>;
private _status: string;
private _errors: {[key: string]: any};
private _controlsErrors: any;
Expand Down Expand Up @@ -88,6 +92,8 @@ export abstract class AbstractControl {

get valueChanges(): Observable<any> { return this._valueChanges; }

get statusChanges(): Observable<any> { return this._statusChanges; }

get pending(): boolean { return this._status == PENDING; }

markAsTouched(): void { this._touched = true; }
Expand Down Expand Up @@ -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) {
Expand All @@ -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}));
}
}

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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});
}
Expand Down Expand Up @@ -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});
}
Expand Down
10 changes: 8 additions & 2 deletions modules/angular2/src/common/forms/validators.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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));
}
Expand Down
4 changes: 4 additions & 0 deletions modules/angular2/src/core/facade/async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> extends Stream<T> {
Expand Down
2 changes: 2 additions & 0 deletions modules/angular2/src/core/facade/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export class ObservableWrapper {
static fromPromise(promise: Promise<any>): Observable<any> {
return RxObservable.fromPromise(promise);
}

static toPromise(obj: Observable<any>): Promise<any> { return (<any>obj).toPromise(); }
}

/**
Expand Down
4 changes: 4 additions & 0 deletions modules/angular2/src/core/facade/promise.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class PromiseWrapper {
async.scheduleMicrotask(fn);
}

static bool isPromise(obj) {
return obj is Future;
}

static PromiseCompleter<dynamic> completer() =>
new PromiseCompleter(new Completer());
}
Expand Down
2 changes: 2 additions & 0 deletions modules/angular2/src/core/facade/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
var resolve;
var reject;
Expand Down
2 changes: 1 addition & 1 deletion modules/angular2/src/core/linker/view_pool.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
60 changes: 56 additions & 4 deletions modules/angular2/test/common/forms/model_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({})) {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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"));

Expand Down Expand Up @@ -185,7 +198,7 @@ export function main() {
}));
});

describe("valueChanges", () => {
describe("valueChanges & statusChanges", () => {
var c;

beforeEach(() => { c = new Control("old", Validators.required); });
Expand All @@ -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",
Expand Down
17 changes: 12 additions & 5 deletions modules/angular2/test/common/forms/validators_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
};
}

Expand Down
4 changes: 4 additions & 0 deletions modules/angular2/test/public_api_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ var NG_ALL = [
'AbstractControl.asyncValidator=',
'AbstractControl.value',
'AbstractControl.valueChanges',
'AbstractControl.statusChanges',
'AbstractControlDirective',
'AbstractControlDirective.control',
'AbstractControlDirective.dirty',
Expand Down Expand Up @@ -307,6 +308,7 @@ var NG_ALL = [
'Control.asyncValidator=',
'Control.value',
'Control.valueChanges',
'Control.statusChanges',
'Control.setErrors()',
'ControlArray',
'ControlArray.at()',
Expand Down Expand Up @@ -339,6 +341,7 @@ var NG_ALL = [
'ControlArray.asyncValidator=',
'ControlArray.value',
'ControlArray.valueChanges',
'ControlArray.statusChanges',
'ControlArray.setErrors()',
'ControlContainer',
'ControlContainer.control',
Expand Down Expand Up @@ -385,6 +388,7 @@ var NG_ALL = [
'ControlGroup.asyncValidator=',
'ControlGroup.value',
'ControlGroup.valueChanges',
'ControlGroup.statusChanges',
'ControlGroup.setErrors()',
'CurrencyPipe',
'CurrencyPipe.transform()',
Expand Down
X Tutup