X Tutup
Skip to content

Commit bb2b961

Browse files
committed
feat(forms): add support for async validations
1 parent 39626a9 commit bb2b961

File tree

8 files changed

+249
-28
lines changed

8 files changed

+249
-28
lines changed

modules/angular2/src/core/facade/promise.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
library angular2.core.facade.promise;
22

33
import 'dart:async';
4+
import 'dart:async' as async;
45
export 'dart:async' show Future;
56

67
class PromiseWrapper {
@@ -29,6 +30,10 @@ class PromiseWrapper {
2930
return promise.catchError(onError);
3031
}
3132

33+
static void scheduleMicrotask(fn) {
34+
async.scheduleMicrotask(fn);
35+
}
36+
3237
static PromiseCompleter<dynamic> completer() =>
3338
new PromiseCompleter(new Completer());
3439
}

modules/angular2/src/core/facade/promise.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export class PromiseWrapper {
4040
});
4141
}
4242

43+
static scheduleMicrotask(computation: () => any): void {
44+
PromiseWrapper.then(PromiseWrapper.resolve(null), computation, (_) => {});
45+
}
46+
4347
static completer(): PromiseCompleter<any> {
4448
var resolve;
4549
var reject;

modules/angular2/src/core/forms/directives/ng_form.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export class NgForm extends ControlContainer implements Form {
105105
get controls(): {[key: string]: AbstractControl} { return this.form.controls; }
106106

107107
addControl(dir: NgControl): void {
108-
this._later(_ => {
108+
PromiseWrapper.scheduleMicrotask(() => {
109109
var container = this._findContainer(dir.path);
110110
var ctrl = new Control();
111111
setUpControl(ctrl, dir);
@@ -117,7 +117,7 @@ export class NgForm extends ControlContainer implements Form {
117117
getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); }
118118

119119
removeControl(dir: NgControl): void {
120-
this._later(_ => {
120+
PromiseWrapper.scheduleMicrotask(() => {
121121
var container = this._findContainer(dir.path);
122122
if (isPresent(container)) {
123123
container.removeControl(dir.name);
@@ -127,7 +127,7 @@ export class NgForm extends ControlContainer implements Form {
127127
}
128128

129129
addControlGroup(dir: NgControlGroup): void {
130-
this._later(_ => {
130+
PromiseWrapper.scheduleMicrotask(() => {
131131
var container = this._findContainer(dir.path);
132132
var group = new ControlGroup({});
133133
setUpControlGroup(group, dir);
@@ -137,7 +137,7 @@ export class NgForm extends ControlContainer implements Form {
137137
}
138138

139139
removeControlGroup(dir: NgControlGroup): void {
140-
this._later(_ => {
140+
PromiseWrapper.scheduleMicrotask(() => {
141141
var container = this._findContainer(dir.path);
142142
if (isPresent(container)) {
143143
container.removeControl(dir.name);
@@ -151,7 +151,7 @@ export class NgForm extends ControlContainer implements Form {
151151
}
152152

153153
updateModel(dir: NgControl, value: any): void {
154-
this._later(_ => {
154+
PromiseWrapper.scheduleMicrotask(() => {
155155
var ctrl = <Control>this.form.find(dir.path);
156156
ctrl.updateValue(value);
157157
});
@@ -167,7 +167,4 @@ export class NgForm extends ControlContainer implements Form {
167167
path.pop();
168168
return ListWrapper.isEmpty(path) ? this.form : <ControlGroup>this.form.find(path);
169169
}
170-
171-
/** @internal */
172-
_later(fn): void { PromiseWrapper.then(PromiseWrapper.resolve(null), fn, (_) => {}); }
173170
}

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

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ export abstract class AbstractControl {
5959
private _pristine: boolean = true;
6060
private _touched: boolean = false;
6161
private _parent: ControlGroup | ControlArray;
62+
private _asyncValidationSubscription;
6263

63-
constructor(public validator: Function) {}
64+
constructor(public validator: Function, public asyncValidator: Function) {}
6465

6566
get value(): any { return this._value; }
6667

@@ -119,10 +120,14 @@ export abstract class AbstractControl {
119120

120121
this._updateValue();
121122

122-
this._errors = this.validator(this);
123+
this._errors = this._runValidator();
123124
this._controlsErrors = this._calculateControlsErrors();
124125
this._status = this._calculateStatus();
125126

127+
if (this._status == VALID || this._status == PENDING) {
128+
this._runAsyncValidator();
129+
}
130+
126131
if (emitEvent) {
127132
ObservableWrapper.callNext(this._valueChanges, this._value);
128133
}
@@ -132,6 +137,23 @@ export abstract class AbstractControl {
132137
}
133138
}
134139

140+
private _runValidator() { return isPresent(this.validator) ? this.validator(this) : null; }
141+
142+
private _runAsyncValidator() {
143+
if (isPresent(this.asyncValidator)) {
144+
this._status = PENDING;
145+
this._cancelExistingSubscription();
146+
this._asyncValidationSubscription =
147+
ObservableWrapper.subscribe(this.asyncValidator(this), res => this.setErrors(res));
148+
}
149+
}
150+
151+
private _cancelExistingSubscription(): void {
152+
if (isPresent(this._asyncValidationSubscription)) {
153+
ObservableWrapper.dispose(this._asyncValidationSubscription);
154+
}
155+
}
156+
135157
/**
136158
* Sets errors on a control.
137159
*
@@ -190,13 +212,18 @@ export abstract class AbstractControl {
190212
}
191213

192214
private _calculateStatus(): string {
193-
return isPresent(this._errors) || isPresent(this._controlsErrors) ? INVALID : VALID;
215+
if (isPresent(this._errors)) return INVALID;
216+
if (this._anyControlsHaveStatus(PENDING)) return PENDING;
217+
if (this._anyControlsHaveStatus(INVALID)) return INVALID;
218+
return VALID;
194219
}
195220

196221
/** @internal */
197222
abstract _updateValue(): void;
198223
/** @internal */
199224
abstract _calculateControlsErrors(): any;
225+
/** @internal */
226+
abstract _anyControlsHaveStatus(status: string): boolean;
200227
}
201228

202229
/**
@@ -219,8 +246,8 @@ export class Control extends AbstractControl {
219246
/** @internal */
220247
_onChange: Function;
221248

222-
constructor(value: any = null, validator: Function = Validators.nullValidator) {
223-
super(validator);
249+
constructor(value: any = null, validator: Function = null, asyncValidator: Function = null) {
250+
super(validator, asyncValidator);
224251
this._value = value;
225252
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
226253
this._valueChanges = new EventEmitter();
@@ -259,6 +286,11 @@ export class Control extends AbstractControl {
259286
*/
260287
_calculateControlsErrors() { return null; }
261288

289+
/**
290+
* @internal
291+
*/
292+
_anyControlsHaveStatus(status: string): boolean { return false; }
293+
262294
/**
263295
* Register a listener for change events.
264296
*/
@@ -282,9 +314,9 @@ export class ControlGroup extends AbstractControl {
282314
private _optionals: {[key: string]: boolean};
283315

284316
constructor(public controls: {[key: string]: AbstractControl},
285-
optionals: {[key: string]: boolean} = null,
286-
validator: Function = Validators.nullValidator) {
287-
super(validator);
317+
optionals: {[key: string]: boolean} = null, validator: Function = null,
318+
asyncValidator: Function = null) {
319+
super(validator, asyncValidator);
288320
this._optionals = isPresent(optionals) ? optionals : {};
289321
this._valueChanges = new EventEmitter();
290322

@@ -348,6 +380,15 @@ export class ControlGroup extends AbstractControl {
348380
return StringMapWrapper.isEmpty(res) ? null : res;
349381
}
350382

383+
/** @internal */
384+
_anyControlsHaveStatus(status: string): boolean {
385+
var res = false;
386+
StringMapWrapper.forEach(this.controls, (control, name) => {
387+
res = res || (this.contains(name) && control.status == status);
388+
});
389+
return res;
390+
}
391+
351392
/** @internal */
352393
_reduceValue() {
353394
return this._reduceChildren({}, (acc, control, name) => {
@@ -396,8 +437,9 @@ export class ControlGroup extends AbstractControl {
396437
* ### Example ([live demo](http://plnkr.co/edit/23DESOpbNnBpBHZt1BR4?p=preview))
397438
*/
398439
export class ControlArray extends AbstractControl {
399-
constructor(public controls: AbstractControl[], validator: Function = Validators.nullValidator) {
400-
super(validator);
440+
constructor(public controls: AbstractControl[], validator: Function = null,
441+
asyncValidator: Function = null) {
442+
super(validator, asyncValidator);
401443

402444
this._valueChanges = new EventEmitter();
403445

@@ -457,6 +499,12 @@ export class ControlArray extends AbstractControl {
457499
return anyErrors ? res : null;
458500
}
459501

502+
/** @internal */
503+
_anyControlsHaveStatus(status: string): boolean {
504+
return ListWrapper.any(this.controls, c => c.status == status);
505+
}
506+
507+
460508
/** @internal */
461509
_setParentForControls(): void {
462510
this.controls.forEach((control) => { control.setParent(this); });

modules/angular2/test/core/forms/form_builder_spec.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,6 @@ export function main() {
5050
expect(g.validator).toBe(Validators.nullValidator);
5151
});
5252

53-
it("should use default validators when no validators are provided", () => {
54-
var g = b.group({"login": "some value"});
55-
expect(g.controls["login"].validator).toBe(Validators.nullValidator);
56-
expect(g.validator).toBe(Validators.nullValidator);
57-
});
58-
5953
it("should create control arrays", () => {
6054
var c = b.control("three");
6155
var a = b.array(["one", ["two", Validators.required], c, b.array(['four'])]);

modules/angular2/test/core/forms/integration_spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {By} from 'angular2/src/core/debug';
3838
import {ListWrapper} from 'angular2/src/core/facade/collection';
3939
import {ObservableWrapper} from 'angular2/src/core/facade/async';
4040
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
41+
import {PromiseWrapper} from "angular2/src/core/facade/promise";
4142

4243
export function main() {
4344
describe("integration tests", () => {
@@ -445,7 +446,7 @@ export function main() {
445446
});
446447
}));
447448

448-
it("should use validators defined in the model",
449+
it("should use sync validators defined in the model",
449450
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
450451
var form = new ControlGroup({"login": new Control("aa", Validators.required)});
451452

@@ -467,6 +468,41 @@ export function main() {
467468
async.done();
468469
});
469470
}));
471+
472+
it("should use async validators defined in the model",
473+
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
474+
var control =
475+
new Control("", Validators.required, uniqLoginAsyncValidator("expected"));
476+
var form = new ControlGroup({"login": control});
477+
478+
var t = `<div [ng-form-model]="form">
479+
<input type="text" ng-control="login">
480+
</div>`;
481+
482+
var rootTC;
483+
tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((root) => rootTC = root);
484+
tick();
485+
486+
rootTC.debugElement.componentInstance.form = form;
487+
rootTC.detectChanges();
488+
489+
expect(form.hasError("required", ["login"])).toEqual(true);
490+
491+
var input = rootTC.debugElement.query(By.css("input"));
492+
input.nativeElement.value = "wrong value";
493+
dispatchEvent(input.nativeElement, "change");
494+
495+
expect(form.pending).toEqual(true);
496+
tick();
497+
498+
expect(form.hasError("uniqLogin", ["login"])).toEqual(true);
499+
500+
input.nativeElement.value = "expected";
501+
dispatchEvent(input.nativeElement, "change");
502+
tick();
503+
504+
expect(form.valid).toEqual(true);
505+
})));
470506
});
471507

472508
describe("nested forms", () => {
@@ -923,6 +959,15 @@ class MyInput implements ControlValueAccessor {
923959
}
924960
}
925961

962+
function uniqLoginAsyncValidator(expectedValue: string) {
963+
return (c) => {
964+
var e = new EventEmitter();
965+
var res = (c.value == expectedValue) ? null : {"uniqLogin": true};
966+
PromiseWrapper.scheduleMicrotask(() => ObservableWrapper.callNext(e, res));
967+
return e;
968+
};
969+
}
970+
926971
function loginIsEmptyGroupValidator(c: ControlGroup) {
927972
return c.controls["login"].value == "" ? {"loginIsEmpty": true} : null;
928973
}

0 commit comments

Comments
 (0)
X Tutup