X Tutup
Skip to content

Commit 4439106

Browse files
committed
feat(facade): add support for async validators returning observables
Closes #5032
1 parent 2c201d3 commit 4439106

File tree

6 files changed

+112
-26
lines changed

6 files changed

+112
-26
lines changed

modules/angular2/src/common/forms/model.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {StringWrapper, isPresent, isBlank, normalizeBool} from 'angular2/src/core/facade/lang';
22
import {Observable, EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
3+
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
34
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
45

56
/**
@@ -42,16 +43,19 @@ function _find(control: AbstractControl, path: Array<string | number>| string) {
4243
}, control);
4344
}
4445

46+
function toObservable(r: any): Observable<any> {
47+
return PromiseWrapper.isPromise(r) ? ObservableWrapper.fromPromise(r) : r;
48+
}
49+
4550
/**
4651
*
4752
*/
4853
export abstract class AbstractControl {
4954
/** @internal */
5055
_value: any;
5156

52-
/** @internal */
53-
_valueChanges: EventEmitter<any>;
54-
57+
private _valueChanges: EventEmitter<any>;
58+
private _statusChanges: EventEmitter<any>;
5559
private _status: string;
5660
private _errors: {[key: string]: any};
5761
private _controlsErrors: any;
@@ -88,6 +92,8 @@ export abstract class AbstractControl {
8892

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

95+
get statusChanges(): Observable<any> { return this._statusChanges; }
96+
9197
get pending(): boolean { return this._status == PENDING; }
9298

9399
markAsTouched(): void { this._touched = true; }
@@ -124,11 +130,12 @@ export abstract class AbstractControl {
124130
this._status = this._calculateStatus();
125131

126132
if (this._status == VALID || this._status == PENDING) {
127-
this._runAsyncValidator();
133+
this._runAsyncValidator(emitEvent);
128134
}
129135

130136
if (emitEvent) {
131137
ObservableWrapper.callNext(this._valueChanges, this._value);
138+
ObservableWrapper.callNext(this._statusChanges, this._status);
132139
}
133140

134141
if (isPresent(this._parent) && !onlySelf) {
@@ -138,13 +145,13 @@ export abstract class AbstractControl {
138145

139146
private _runValidator() { return isPresent(this.validator) ? this.validator(this) : null; }
140147

141-
private _runAsyncValidator() {
148+
private _runAsyncValidator(emitEvent: boolean): void {
142149
if (isPresent(this.asyncValidator)) {
143150
this._status = PENDING;
144151
this._cancelExistingSubscription();
145-
var obs = ObservableWrapper.fromPromise(this.asyncValidator(this));
152+
var obs = toObservable(this.asyncValidator(this));
146153
this._asyncValidationSubscription =
147-
ObservableWrapper.subscribe(obs, res => this.setErrors(res));
154+
ObservableWrapper.subscribe(obs, res => this.setErrors(res, {emitEvent: emitEvent}));
148155
}
149156
}
150157

@@ -177,10 +184,16 @@ export abstract class AbstractControl {
177184
* expect(login.valid).toEqual(true);
178185
* ```
179186
*/
180-
setErrors(errors: {[key: string]: any}): void {
187+
setErrors(errors: {[key: string]: any}, {emitEvent}: {emitEvent?: boolean} = {}): void {
188+
emitEvent = isPresent(emitEvent) ? emitEvent : true;
189+
181190
this._errors = errors;
182191
this._status = this._calculateStatus();
183192

193+
if (emitEvent) {
194+
ObservableWrapper.callNext(this._statusChanges, this._status);
195+
}
196+
184197
if (isPresent(this._parent)) {
185198
this._parent._updateControlsErrors();
186199
}
@@ -211,6 +224,13 @@ export abstract class AbstractControl {
211224
}
212225
}
213226

227+
/** @internal */
228+
_initObservables() {
229+
this._valueChanges = new EventEmitter();
230+
this._statusChanges = new EventEmitter();
231+
}
232+
233+
214234
private _calculateStatus(): string {
215235
if (isPresent(this._errors)) return INVALID;
216236
if (this._anyControlsHaveStatus(PENDING)) return PENDING;
@@ -250,7 +270,7 @@ export class Control extends AbstractControl {
250270
super(validator, asyncValidator);
251271
this._value = value;
252272
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
253-
this._valueChanges = new EventEmitter();
273+
this._initObservables();
254274
}
255275

256276
/**
@@ -318,8 +338,7 @@ export class ControlGroup extends AbstractControl {
318338
asyncValidator: Function = null) {
319339
super(validator, asyncValidator);
320340
this._optionals = isPresent(optionals) ? optionals : {};
321-
this._valueChanges = new EventEmitter();
322-
341+
this._initObservables();
323342
this._setParentForControls();
324343
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
325344
}
@@ -440,9 +459,7 @@ export class ControlArray extends AbstractControl {
440459
constructor(public controls: AbstractControl[], validator: Function = null,
441460
asyncValidator: Function = null) {
442461
super(validator, asyncValidator);
443-
444-
this._valueChanges = new EventEmitter();
445-
462+
this._initObservables();
446463
this._setParentForControls();
447464
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
448465
}

modules/angular2/src/common/forms/validators.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {isBlank, isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang';
22
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
3+
import {ObservableWrapper} from 'angular2/src/core/facade/async';
34
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
45
import {OpaqueToken} from 'angular2/src/core/di';
56

@@ -89,15 +90,20 @@ export class Validators {
8990

9091
static composeAsync(validators: Function[]): Function {
9192
if (isBlank(validators)) return null;
92-
var presentValidators = ListWrapper.filter(validators, isPresent);
93+
let presentValidators = ListWrapper.filter(validators, isPresent);
9394
if (presentValidators.length == 0) return null;
9495

9596
return function(control: modelModule.AbstractControl) {
96-
return PromiseWrapper.all(_executeValidators(control, presentValidators)).then(_mergeErrors);
97+
let promises = _executeValidators(control, presentValidators).map(_convertToPromise);
98+
return PromiseWrapper.all(promises).then(_mergeErrors);
9799
};
98100
}
99101
}
100102

103+
function _convertToPromise(obj: any): any {
104+
return PromiseWrapper.isPromise(obj) ? obj : ObservableWrapper.toPromise(obj);
105+
}
106+
101107
function _executeValidators(control: modelModule.AbstractControl, validators: Function[]): any[] {
102108
return validators.map(v => v(control));
103109
}

modules/angular2/src/core/linker/view_pool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Inject, Injectable, OpaqueToken} from 'angular2/src/core/di';
22

3-
import {MapWrapper, Map} from 'angular2/src/core/facade/collection';
43
import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang';
4+
import {MapWrapper, Map} from 'angular2/src/core/facade/collection';
55

66
import * as viewModule from './view';
77

modules/angular2/test/common/forms/model_spec.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ import {
1414
inject
1515
} from 'angular2/testing_internal';
1616
import {ControlGroup, Control, ControlArray, Validators} from 'angular2/core';
17-
import {isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang';
17+
import {IS_DART, isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang';
1818
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
19-
import {TimerWrapper, ObservableWrapper} from 'angular2/src/core/facade/async';
20-
import {IS_DART} from 'angular2/src/core/facade/lang';
19+
import {TimerWrapper, ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async';
2120

2221
export function main() {
2322
function asyncValidator(expected, timeouts = CONST_EXPR({})) {
@@ -36,6 +35,12 @@ export function main() {
3635
};
3736
}
3837

38+
function asyncValidatorReturningObservable(c) {
39+
var e = new EventEmitter();
40+
PromiseWrapper.scheduleMicrotask(() => ObservableWrapper.callNext(e, {"async": true}));
41+
return e;
42+
}
43+
3944
describe("Form Model", () => {
4045
describe("Control", () => {
4146
it("should default the value to null", () => {
@@ -70,6 +75,14 @@ export function main() {
7075
expect(c.errors).toEqual({"async": true});
7176
}));
7277

78+
it("should support validators returning observables", fakeAsync(() => {
79+
var c = new Control("value", null, asyncValidatorReturningObservable);
80+
tick();
81+
82+
expect(c.valid).toEqual(false);
83+
expect(c.errors).toEqual({"async": true});
84+
}));
85+
7386
it("should rerun the validator when the value changes", fakeAsync(() => {
7487
var c = new Control("value", null, asyncValidator("expected"));
7588

@@ -185,7 +198,7 @@ export function main() {
185198
}));
186199
});
187200

188-
describe("valueChanges", () => {
201+
describe("valueChanges & statusChanges", () => {
189202
var c;
190203

191204
beforeEach(() => { c = new Control("old", Validators.required); });
@@ -200,6 +213,45 @@ export function main() {
200213
c.updateValue("new");
201214
}));
202215

216+
it("should fire an event after the status has been updated to invalid", fakeAsync(() => {
217+
ObservableWrapper.subscribe(c.statusChanges, (status) => {
218+
expect(c.status).toEqual('INVALID');
219+
expect(status).toEqual('INVALID');
220+
});
221+
222+
c.updateValue("");
223+
tick();
224+
}));
225+
226+
it("should fire an event after the status has been updated to pending", fakeAsync(() => {
227+
var c = new Control("old", Validators.required, asyncValidator("expected"));
228+
229+
var log = [];
230+
ObservableWrapper.subscribe(c.valueChanges, (value) => log.push(`value: '${value}'`));
231+
ObservableWrapper.subscribe(c.statusChanges,
232+
(status) => log.push(`status: '${status}'`));
233+
234+
c.updateValue("");
235+
tick();
236+
237+
c.updateValue("nonEmpty");
238+
tick();
239+
240+
c.updateValue("expected");
241+
tick();
242+
243+
expect(log).toEqual([
244+
"" + "value: ''",
245+
"status: 'INVALID'",
246+
"value: 'nonEmpty'",
247+
"status: 'PENDING'",
248+
"status: 'INVALID'",
249+
"value: 'expected'",
250+
"status: 'PENDING'",
251+
"status: 'VALID'",
252+
]);
253+
}));
254+
203255
// TODO: remove the if statement after making observable delivery sync
204256
if (!IS_DART) {
205257
it("should update set errors and status before emitting an event",

modules/angular2/test/common/forms/validators_spec.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from 'angular2/testing_internal';
1414
import {ControlGroup, Control, Validators, AbstractControl, ControlArray} from 'angular2/core';
1515
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
16-
import {TimerWrapper} from 'angular2/src/core/facade/async';
16+
import {EventEmitter, ObservableWrapper, TimerWrapper} from 'angular2/src/core/facade/async';
1717
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
1818

1919
export function main() {
@@ -95,12 +95,19 @@ export function main() {
9595
});
9696

9797
describe("composeAsync", () => {
98-
function asyncValidator(expected, response, timeout = 0) {
98+
function asyncValidator(expected, response) {
9999
return (c) => {
100-
var completer = PromiseWrapper.completer();
100+
var emitter = new EventEmitter();
101101
var res = c.value != expected ? response : null;
102-
TimerWrapper.setTimeout(() => { completer.resolve(res); }, timeout);
103-
return completer.promise;
102+
103+
PromiseWrapper.scheduleMicrotask(() => {
104+
ObservableWrapper.callNext(emitter, res);
105+
// this is required because of a bug in ObservableWrapper
106+
// where callComplete can fire before callNext
107+
// remove this one the bug is fixed
108+
TimerWrapper.setTimeout(() => { ObservableWrapper.callComplete(emitter); }, 0);
109+
});
110+
return emitter;
104111
};
105112
}
106113

modules/angular2/test/public_api_spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ var NG_ALL = [
6565
'AbstractControl.asyncValidator=',
6666
'AbstractControl.value',
6767
'AbstractControl.valueChanges',
68+
'AbstractControl.statusChanges',
6869
'AbstractControlDirective',
6970
'AbstractControlDirective.control',
7071
'AbstractControlDirective.dirty',
@@ -307,6 +308,7 @@ var NG_ALL = [
307308
'Control.asyncValidator=',
308309
'Control.value',
309310
'Control.valueChanges',
311+
'Control.statusChanges',
310312
'Control.setErrors()',
311313
'ControlArray',
312314
'ControlArray.at()',
@@ -339,6 +341,7 @@ var NG_ALL = [
339341
'ControlArray.asyncValidator=',
340342
'ControlArray.value',
341343
'ControlArray.valueChanges',
344+
'ControlArray.statusChanges',
342345
'ControlArray.setErrors()',
343346
'ControlContainer',
344347
'ControlContainer.control',
@@ -385,6 +388,7 @@ var NG_ALL = [
385388
'ControlGroup.asyncValidator=',
386389
'ControlGroup.value',
387390
'ControlGroup.valueChanges',
391+
'ControlGroup.statusChanges',
388392
'ControlGroup.setErrors()',
389393
'CurrencyPipe',
390394
'CurrencyPipe.transform()',

0 commit comments

Comments
 (0)
X Tutup