feat(facade): add support for async validators returning observables
Closes #5032
This commit is contained in:
parent
2c201d3f34
commit
4439106a1f
|
@ -1,5 +1,6 @@
|
||||||
import {StringWrapper, isPresent, isBlank, normalizeBool} from 'angular2/src/core/facade/lang';
|
import {StringWrapper, isPresent, isBlank, normalizeBool} from 'angular2/src/core/facade/lang';
|
||||||
import {Observable, EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
|
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';
|
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,6 +43,10 @@ function _find(control: AbstractControl, path: Array<string | number>| string) {
|
||||||
}, control);
|
}, control);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toObservable(r: any): Observable<any> {
|
||||||
|
return PromiseWrapper.isPromise(r) ? ObservableWrapper.fromPromise(r) : r;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@ -49,9 +54,8 @@ export abstract class AbstractControl {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_value: any;
|
_value: any;
|
||||||
|
|
||||||
/** @internal */
|
private _valueChanges: EventEmitter<any>;
|
||||||
_valueChanges: EventEmitter<any>;
|
private _statusChanges: EventEmitter<any>;
|
||||||
|
|
||||||
private _status: string;
|
private _status: string;
|
||||||
private _errors: {[key: string]: any};
|
private _errors: {[key: string]: any};
|
||||||
private _controlsErrors: any;
|
private _controlsErrors: any;
|
||||||
|
@ -88,6 +92,8 @@ export abstract class AbstractControl {
|
||||||
|
|
||||||
get valueChanges(): Observable<any> { return this._valueChanges; }
|
get valueChanges(): Observable<any> { return this._valueChanges; }
|
||||||
|
|
||||||
|
get statusChanges(): Observable<any> { return this._statusChanges; }
|
||||||
|
|
||||||
get pending(): boolean { return this._status == PENDING; }
|
get pending(): boolean { return this._status == PENDING; }
|
||||||
|
|
||||||
markAsTouched(): void { this._touched = true; }
|
markAsTouched(): void { this._touched = true; }
|
||||||
|
@ -124,11 +130,12 @@ export abstract class AbstractControl {
|
||||||
this._status = this._calculateStatus();
|
this._status = this._calculateStatus();
|
||||||
|
|
||||||
if (this._status == VALID || this._status == PENDING) {
|
if (this._status == VALID || this._status == PENDING) {
|
||||||
this._runAsyncValidator();
|
this._runAsyncValidator(emitEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emitEvent) {
|
if (emitEvent) {
|
||||||
ObservableWrapper.callNext(this._valueChanges, this._value);
|
ObservableWrapper.callNext(this._valueChanges, this._value);
|
||||||
|
ObservableWrapper.callNext(this._statusChanges, this._status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPresent(this._parent) && !onlySelf) {
|
if (isPresent(this._parent) && !onlySelf) {
|
||||||
|
@ -138,13 +145,13 @@ export abstract class AbstractControl {
|
||||||
|
|
||||||
private _runValidator() { return isPresent(this.validator) ? this.validator(this) : null; }
|
private _runValidator() { return isPresent(this.validator) ? this.validator(this) : null; }
|
||||||
|
|
||||||
private _runAsyncValidator() {
|
private _runAsyncValidator(emitEvent: boolean): void {
|
||||||
if (isPresent(this.asyncValidator)) {
|
if (isPresent(this.asyncValidator)) {
|
||||||
this._status = PENDING;
|
this._status = PENDING;
|
||||||
this._cancelExistingSubscription();
|
this._cancelExistingSubscription();
|
||||||
var obs = ObservableWrapper.fromPromise(this.asyncValidator(this));
|
var obs = toObservable(this.asyncValidator(this));
|
||||||
this._asyncValidationSubscription =
|
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);
|
* 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._errors = errors;
|
||||||
this._status = this._calculateStatus();
|
this._status = this._calculateStatus();
|
||||||
|
|
||||||
|
if (emitEvent) {
|
||||||
|
ObservableWrapper.callNext(this._statusChanges, this._status);
|
||||||
|
}
|
||||||
|
|
||||||
if (isPresent(this._parent)) {
|
if (isPresent(this._parent)) {
|
||||||
this._parent._updateControlsErrors();
|
this._parent._updateControlsErrors();
|
||||||
}
|
}
|
||||||
|
@ -211,6 +224,13 @@ export abstract class AbstractControl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_initObservables() {
|
||||||
|
this._valueChanges = new EventEmitter();
|
||||||
|
this._statusChanges = new EventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private _calculateStatus(): string {
|
private _calculateStatus(): string {
|
||||||
if (isPresent(this._errors)) return INVALID;
|
if (isPresent(this._errors)) return INVALID;
|
||||||
if (this._anyControlsHaveStatus(PENDING)) return PENDING;
|
if (this._anyControlsHaveStatus(PENDING)) return PENDING;
|
||||||
|
@ -250,7 +270,7 @@ export class Control extends AbstractControl {
|
||||||
super(validator, asyncValidator);
|
super(validator, asyncValidator);
|
||||||
this._value = value;
|
this._value = value;
|
||||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||||
this._valueChanges = new EventEmitter();
|
this._initObservables();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -318,8 +338,7 @@ export class ControlGroup extends AbstractControl {
|
||||||
asyncValidator: Function = null) {
|
asyncValidator: Function = null) {
|
||||||
super(validator, asyncValidator);
|
super(validator, asyncValidator);
|
||||||
this._optionals = isPresent(optionals) ? optionals : {};
|
this._optionals = isPresent(optionals) ? optionals : {};
|
||||||
this._valueChanges = new EventEmitter();
|
this._initObservables();
|
||||||
|
|
||||||
this._setParentForControls();
|
this._setParentForControls();
|
||||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||||
}
|
}
|
||||||
|
@ -440,9 +459,7 @@ export class ControlArray extends AbstractControl {
|
||||||
constructor(public controls: AbstractControl[], validator: Function = null,
|
constructor(public controls: AbstractControl[], validator: Function = null,
|
||||||
asyncValidator: Function = null) {
|
asyncValidator: Function = null) {
|
||||||
super(validator, asyncValidator);
|
super(validator, asyncValidator);
|
||||||
|
this._initObservables();
|
||||||
this._valueChanges = new EventEmitter();
|
|
||||||
|
|
||||||
this._setParentForControls();
|
this._setParentForControls();
|
||||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {isBlank, isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang';
|
import {isBlank, isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||||
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
|
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 {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
|
||||||
import {OpaqueToken} from 'angular2/src/core/di';
|
import {OpaqueToken} from 'angular2/src/core/di';
|
||||||
|
|
||||||
|
@ -89,15 +90,20 @@ export class Validators {
|
||||||
|
|
||||||
static composeAsync(validators: Function[]): Function {
|
static composeAsync(validators: Function[]): Function {
|
||||||
if (isBlank(validators)) return null;
|
if (isBlank(validators)) return null;
|
||||||
var presentValidators = ListWrapper.filter(validators, isPresent);
|
let presentValidators = ListWrapper.filter(validators, isPresent);
|
||||||
if (presentValidators.length == 0) return null;
|
if (presentValidators.length == 0) return null;
|
||||||
|
|
||||||
return function(control: modelModule.AbstractControl) {
|
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[] {
|
function _executeValidators(control: modelModule.AbstractControl, validators: Function[]): any[] {
|
||||||
return validators.map(v => v(control));
|
return validators.map(v => v(control));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Inject, Injectable, OpaqueToken} from 'angular2/src/core/di';
|
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 {isPresent, isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||||
|
import {MapWrapper, Map} from 'angular2/src/core/facade/collection';
|
||||||
|
|
||||||
import * as viewModule from './view';
|
import * as viewModule from './view';
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,9 @@ import {
|
||||||
inject
|
inject
|
||||||
} from 'angular2/testing_internal';
|
} from 'angular2/testing_internal';
|
||||||
import {ControlGroup, Control, ControlArray, Validators} from 'angular2/core';
|
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 {PromiseWrapper} from 'angular2/src/core/facade/promise';
|
||||||
import {TimerWrapper, ObservableWrapper} from 'angular2/src/core/facade/async';
|
import {TimerWrapper, ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async';
|
||||||
import {IS_DART} from 'angular2/src/core/facade/lang';
|
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
function asyncValidator(expected, timeouts = CONST_EXPR({})) {
|
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("Form Model", () => {
|
||||||
describe("Control", () => {
|
describe("Control", () => {
|
||||||
it("should default the value to null", () => {
|
it("should default the value to null", () => {
|
||||||
|
@ -70,6 +75,14 @@ export function main() {
|
||||||
expect(c.errors).toEqual({"async": true});
|
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(() => {
|
it("should rerun the validator when the value changes", fakeAsync(() => {
|
||||||
var c = new Control("value", null, asyncValidator("expected"));
|
var c = new Control("value", null, asyncValidator("expected"));
|
||||||
|
|
||||||
|
@ -185,7 +198,7 @@ export function main() {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("valueChanges", () => {
|
describe("valueChanges & statusChanges", () => {
|
||||||
var c;
|
var c;
|
||||||
|
|
||||||
beforeEach(() => { c = new Control("old", Validators.required); });
|
beforeEach(() => { c = new Control("old", Validators.required); });
|
||||||
|
@ -200,6 +213,45 @@ export function main() {
|
||||||
c.updateValue("new");
|
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
|
// TODO: remove the if statement after making observable delivery sync
|
||||||
if (!IS_DART) {
|
if (!IS_DART) {
|
||||||
it("should update set errors and status before emitting an event",
|
it("should update set errors and status before emitting an event",
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from 'angular2/testing_internal';
|
} from 'angular2/testing_internal';
|
||||||
import {ControlGroup, Control, Validators, AbstractControl, ControlArray} from 'angular2/core';
|
import {ControlGroup, Control, Validators, AbstractControl, ControlArray} from 'angular2/core';
|
||||||
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
|
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';
|
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
|
@ -95,12 +95,19 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("composeAsync", () => {
|
describe("composeAsync", () => {
|
||||||
function asyncValidator(expected, response, timeout = 0) {
|
function asyncValidator(expected, response) {
|
||||||
return (c) => {
|
return (c) => {
|
||||||
var completer = PromiseWrapper.completer();
|
var emitter = new EventEmitter();
|
||||||
var res = c.value != expected ? response : null;
|
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ var NG_ALL = [
|
||||||
'AbstractControl.asyncValidator=',
|
'AbstractControl.asyncValidator=',
|
||||||
'AbstractControl.value',
|
'AbstractControl.value',
|
||||||
'AbstractControl.valueChanges',
|
'AbstractControl.valueChanges',
|
||||||
|
'AbstractControl.statusChanges',
|
||||||
'AbstractControlDirective',
|
'AbstractControlDirective',
|
||||||
'AbstractControlDirective.control',
|
'AbstractControlDirective.control',
|
||||||
'AbstractControlDirective.dirty',
|
'AbstractControlDirective.dirty',
|
||||||
|
@ -307,6 +308,7 @@ var NG_ALL = [
|
||||||
'Control.asyncValidator=',
|
'Control.asyncValidator=',
|
||||||
'Control.value',
|
'Control.value',
|
||||||
'Control.valueChanges',
|
'Control.valueChanges',
|
||||||
|
'Control.statusChanges',
|
||||||
'Control.setErrors()',
|
'Control.setErrors()',
|
||||||
'ControlArray',
|
'ControlArray',
|
||||||
'ControlArray.at()',
|
'ControlArray.at()',
|
||||||
|
@ -339,6 +341,7 @@ var NG_ALL = [
|
||||||
'ControlArray.asyncValidator=',
|
'ControlArray.asyncValidator=',
|
||||||
'ControlArray.value',
|
'ControlArray.value',
|
||||||
'ControlArray.valueChanges',
|
'ControlArray.valueChanges',
|
||||||
|
'ControlArray.statusChanges',
|
||||||
'ControlArray.setErrors()',
|
'ControlArray.setErrors()',
|
||||||
'ControlContainer',
|
'ControlContainer',
|
||||||
'ControlContainer.control',
|
'ControlContainer.control',
|
||||||
|
@ -385,6 +388,7 @@ var NG_ALL = [
|
||||||
'ControlGroup.asyncValidator=',
|
'ControlGroup.asyncValidator=',
|
||||||
'ControlGroup.value',
|
'ControlGroup.value',
|
||||||
'ControlGroup.valueChanges',
|
'ControlGroup.valueChanges',
|
||||||
|
'ControlGroup.statusChanges',
|
||||||
'ControlGroup.setErrors()',
|
'ControlGroup.setErrors()',
|
||||||
'CurrencyPipe',
|
'CurrencyPipe',
|
||||||
'CurrencyPipe.transform()',
|
'CurrencyPipe.transform()',
|
||||||
|
|
Loading…
Reference in New Issue