feat(forms): add support for adding async validators via template

Example:

@Directive({
  selector: '[uniq-login-validator]',
  providers: [provide(NG_ASYNC_VALIDATORS, {useExisting: UniqLoginValidator, multi: true})]
})
class UniqLoginValidator implements Validator {
  validate(c) { return someFunctionReturningPromiseOrObservable(); }
}
This commit is contained in:
vsavkin
2015-11-02 10:00:42 -08:00
committed by Victor Savkin
parent cf449ddaa9
commit 31c12af81f
14 changed files with 249 additions and 101 deletions

View File

@ -11,7 +11,8 @@ import {
afterEach,
el,
AsyncTestCompleter,
inject
inject,
tick
} from 'angular2/testing_internal';
import {SpyNgControl, SpyValueAccessor} from '../spies';
@ -38,7 +39,8 @@ import {
import {selectValueAccessor, composeValidators} from 'angular2/src/core/forms/directives/shared';
import {TimerWrapper} from 'angular2/src/core/facade/async';
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
import {SimpleChange} from 'angular2/src/core/change_detection';
class DummyControlValueAccessor implements ControlValueAccessor {
@ -54,6 +56,19 @@ class CustomValidatorDirective implements Validator {
validate(c: Control): {[key: string]: any} { return {"custom": true}; }
}
function asyncValidator(expected, timeout = 0) {
return (c) => {
var completer = PromiseWrapper.completer();
var res = c.value != expected ? {"async": true} : null;
if (timeout == 0) {
completer.resolve(res);
} else {
TimerWrapper.setTimeout(() => { completer.resolve(res); }, timeout);
}
return completer.promise;
};
}
export function main() {
describe("Form Directives", () => {
var defaultAccessor;
@ -125,7 +140,7 @@ export function main() {
var loginControlDir;
beforeEach(() => {
form = new NgFormModel([]);
form = new NgFormModel([], []);
formModel = new ControlGroup({
"login": new Control(),
"passwords":
@ -133,7 +148,8 @@ export function main() {
});
form.form = formModel;
loginControlDir = new NgControlName(form, [Validators.required], [defaultAccessor]);
loginControlDir = new NgControlName(form, [Validators.required],
[asyncValidator("expected")], [defaultAccessor]);
loginControlDir.name = "login";
loginControlDir.valueAccessor = new DummyControlValueAccessor();
});
@ -151,7 +167,7 @@ export function main() {
describe("addControl", () => {
it("should throw when no control found", () => {
var dir = new NgControlName(form, null, [defaultAccessor]);
var dir = new NgControlName(form, null, null, [defaultAccessor]);
dir.name = "invalidName";
expect(() => form.addControl(dir))
@ -159,21 +175,30 @@ export function main() {
});
it("should throw when no value accessor", () => {
var dir = new NgControlName(form, null, null);
var dir = new NgControlName(form, null, null, null);
dir.name = "login";
expect(() => form.addControl(dir))
.toThrowError(new RegExp("No value accessor for 'login'"));
});
it("should set up validator", () => {
expect(formModel.find(["login"]).valid).toBe(true);
it("should set up validators", fakeAsync(() => {
form.addControl(loginControlDir);
// this will add the required validator and recalculate the validity
form.addControl(loginControlDir);
// sync validators are set
expect(formModel.hasError("required", ["login"])).toBe(true);
expect(formModel.hasError("async", ["login"])).toBe(false);
expect(formModel.find(["login"]).valid).toBe(false);
});
formModel.find(["login"]).updateValue("invalid value");
// sync validator passes, running async validators
expect(formModel.pending).toBe(true);
tick();
expect(formModel.hasError("required", ["login"])).toBe(false);
expect(formModel.hasError("async", ["login"])).toBe(true);
}));
it("should write value to the DOM", () => {
formModel.find(["login"]).updateValue("initValue");
@ -198,15 +223,27 @@ export function main() {
}
};
it("should set up validator", () => {
var group = new NgControlGroup(form, [matchingPasswordsValidator]);
group.name = "passwords";
form.addControlGroup(group);
it("should set up validator", fakeAsync(() => {
var group = new NgControlGroup(form, [matchingPasswordsValidator],
[asyncValidator('expected')]);
group.name = "passwords";
form.addControlGroup(group);
formModel.find(["passwords", "password"]).updateValue("somePassword");
formModel.find(["passwords", "password"]).updateValue("somePassword");
formModel.find(["passwords", "passwordConfirm"]).updateValue("someOtherPassword");
expect(formModel.hasError("differentPasswords", ["passwords"])).toEqual(true);
});
// sync validators are set
expect(formModel.hasError("differentPasswords", ["passwords"])).toEqual(true);
formModel.find(["passwords", "passwordConfirm"]).updateValue("somePassword");
// sync validators pass, running async validators
expect(formModel.pending).toBe(true);
tick();
expect(formModel.hasError("async", ["passwords"])).toBe(true);
}));
});
describe("removeControl", () => {
@ -228,17 +265,24 @@ export function main() {
expect((<any>loginControlDir.valueAccessor).writtenValue).toEqual("new value");
});
it("should set up validator", () => {
it("should set up a sync validator", () => {
var formValidator = (c) => ({"custom": true});
var f = new NgFormModel([formValidator]);
var f = new NgFormModel([formValidator], []);
f.form = formModel;
f.onChanges({"form": formModel});
// trigger validation
formModel.controls["login"].updateValue("");
expect(formModel.errors).toEqual({"custom": true});
});
it("should set up an async validator", fakeAsync(() => {
var f = new NgFormModel([], [asyncValidator("expected")]);
f.form = formModel;
f.onChanges({"form": formModel});
tick();
expect(formModel.errors).toEqual({"async": true});
}));
});
});
@ -249,13 +293,13 @@ export function main() {
var personControlGroupDir;
beforeEach(() => {
form = new NgForm([]);
form = new NgForm([], []);
formModel = form.form;
personControlGroupDir = new NgControlGroup(form, []);
personControlGroupDir = new NgControlGroup(form, [], []);
personControlGroupDir.name = "person";
loginControlDir = new NgControlName(personControlGroupDir, null, [defaultAccessor]);
loginControlDir = new NgControlName(personControlGroupDir, null, null, [defaultAccessor]);
loginControlDir.name = "login";
loginControlDir.valueAccessor = new DummyControlValueAccessor();
});
@ -301,16 +345,22 @@ export function main() {
// should update the form's value and validity
});
it("should set up validator", fakeAsync(() => {
it("should set up sync validator", fakeAsync(() => {
var formValidator = (c) => ({"custom": true});
var f = new NgForm([formValidator]);
f.addControlGroup(personControlGroupDir);
f.addControl(loginControlDir);
var f = new NgForm([formValidator], []);
flushMicrotasks();
tick();
expect(f.form.errors).toEqual({"custom": true});
}));
it("should set up async validator", fakeAsync(() => {
var f = new NgForm([], [asyncValidator("expected")]);
tick();
expect(f.form.errors).toEqual({"async": true});
}));
});
describe("NgControlGroup", () => {
@ -320,9 +370,9 @@ export function main() {
beforeEach(() => {
formModel = new ControlGroup({"login": new Control(null)});
var parent = new NgFormModel([]);
var parent = new NgFormModel([], []);
parent.form = new ControlGroup({"group": formModel});
controlGroupDir = new NgControlGroup(parent, []);
controlGroupDir = new NgControlGroup(parent, [], []);
controlGroupDir.name = "group";
});
@ -353,7 +403,7 @@ export function main() {
};
beforeEach(() => {
controlDir = new NgFormControl([Validators.required], [defaultAccessor]);
controlDir = new NgFormControl([Validators.required], [], [defaultAccessor]);
controlDir.valueAccessor = new DummyControlValueAccessor();
control = new Control(null);
@ -384,7 +434,8 @@ export function main() {
var ngModel;
beforeEach(() => {
ngModel = new NgModel([Validators.required], [defaultAccessor]);
ngModel =
new NgModel([Validators.required], [asyncValidator("expected")], [defaultAccessor]);
ngModel.valueAccessor = new DummyControlValueAccessor();
});
@ -400,14 +451,18 @@ export function main() {
expect(ngModel.untouched).toBe(control.untouched);
});
it("should set up validator", () => {
expect(ngModel.control.valid).toBe(true);
it("should set up validator", fakeAsync(() => {
// this will add the required validator and recalculate the validity
ngModel.onChanges({});
tick();
// this will add the required validator and recalculate the validity
ngModel.onChanges({});
expect(ngModel.control.errors).toEqual({"required": true});
expect(ngModel.control.valid).toBe(false);
});
ngModel.control.updateValue("someValue");
tick();
expect(ngModel.control.errors).toEqual({"async": true});
}));
});
describe("NgControlName", () => {
@ -417,9 +472,9 @@ export function main() {
beforeEach(() => {
formModel = new Control("name");
var parent = new NgFormModel([]);
var parent = new NgFormModel([], []);
parent.form = new ControlGroup({"name": formModel});
controlNameDir = new NgControlName(parent, [], [defaultAccessor]);
controlNameDir = new NgControlName(parent, [], [], [defaultAccessor]);
controlNameDir.name = "name";
});

View File

@ -20,11 +20,13 @@ import {
import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {
Input,
Control,
ControlGroup,
ControlValueAccessor,
FORM_DIRECTIVES,
NG_VALIDATORS,
NG_ASYNC_VALIDATORS,
Provider,
NgControl,
NgIf,
@ -401,7 +403,7 @@ export function main() {
});
describe("validations", () => {
it("should use validators defined in html",
it("should use sync validators defined in html",
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
var form = new ControlGroup(
{"login": new Control(""), "min": new Control(""), "max": new Control("")});
@ -446,6 +448,35 @@ export function main() {
});
}));
it("should use async validators defined in the html",
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
var form = new ControlGroup({"login": new Control("")});
var t = `<div [ng-form-model]="form">
<input type="text" ng-control="login" uniq-login-validator="expected">
</div>`;
var rootTC;
tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((root) => rootTC = root);
tick();
rootTC.debugElement.componentInstance.form = form;
rootTC.detectChanges();
expect(form.pending).toEqual(true);
tick(100);
expect(form.hasError("uniqLogin", ["login"])).toEqual(true);
var input = rootTC.debugElement.query(By.css("input"));
input.nativeElement.value = "expected";
dispatchEvent(input.nativeElement, "change");
tick(100);
expect(form.valid).toEqual(true);
})));
it("should use sync validators defined in the model",
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
var form = new ControlGroup({"login": new Control("aa", Validators.required)});
@ -961,10 +992,10 @@ class MyInput implements ControlValueAccessor {
function uniqLoginAsyncValidator(expectedValue: string) {
return (c) => {
var e = new EventEmitter();
var completer = PromiseWrapper.completer();
var res = (c.value == expectedValue) ? null : {"uniqLogin": true};
PromiseWrapper.scheduleMicrotask(() => ObservableWrapper.callNext(e, res));
return e;
completer.resolve(res);
return completer.promise;
};
}
@ -979,10 +1010,31 @@ function loginIsEmptyGroupValidator(c: ControlGroup) {
class LoginIsEmptyValidator {
}
@Directive({
selector: '[uniq-login-validator]',
providers: [
new Provider(NG_ASYNC_VALIDATORS,
{useExisting: forwardRef(() => UniqLoginValidator), multi: true})
]
})
class UniqLoginValidator implements Validator {
@Input('uniq-login-validator') expected;
validate(c) { return uniqLoginAsyncValidator(this.expected)(c); }
}
@Component({
selector: "my-comp",
template: '',
directives: [FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator]
directives: [
FORM_DIRECTIVES,
WrappedValue,
MyInput,
NgIf,
NgFor,
LoginIsEmptyValidator,
UniqLoginValidator
]
})
class MyComp {
form: any;

View File

@ -15,23 +15,24 @@ import {
} from 'angular2/testing_internal';
import {ControlGroup, Control, ControlArray, Validators} from 'angular2/core';
import {isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang';
import {EventEmitter, TimerWrapper, ObservableWrapper} from 'angular2/src/core/facade/async';
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
import {TimerWrapper, ObservableWrapper} from 'angular2/src/core/facade/async';
import {IS_DART} from '../../platform';
import {PromiseWrapper} from "angular2/src/core/facade/promise";
export function main() {
function asyncValidator(expected, timeouts = CONST_EXPR({})) {
return (c) => {
var e = new EventEmitter();
var completer = PromiseWrapper.completer();
var t = isPresent(timeouts[c.value]) ? timeouts[c.value] : 0;
var res = c.value != expected ? {"async": true} : null;
if (t == 0) {
PromiseWrapper.scheduleMicrotask(() => { ObservableWrapper.callNext(e, res); });
completer.resolve(res);
} else {
TimerWrapper.setTimeout(() => { ObservableWrapper.callNext(e, res); }, t);
TimerWrapper.setTimeout(() => { completer.resolve(res); }, t);
}
return e;
return completer.promise;
};
}

View File

@ -819,6 +819,7 @@ var NG_ALL = [
'LowerCasePipe',
'LowerCasePipe.transform()',
'NG_VALIDATORS',
'NG_ASYNC_VALIDATORS',
'NgClass',
'NgClass.doCheck()',
'NgClass.initialClasses=',
@ -837,6 +838,7 @@ var NG_ALL = [
'NgControl.untouched',
'NgControl.valid',
'NgControl.validator',
'NgControl.asyncValidator',
'NgControl.value',
'NgControl.valueAccessor',
'NgControl.valueAccessor=',
@ -857,6 +859,7 @@ var NG_ALL = [
'NgControlGroup.valid',
'NgControlGroup.value',
'NgControlGroup.validator',
'NgControlGroup.asyncValidator',
'NgControlStatus',
'NgControlStatus.ngClassDirty',
'NgControlStatus.ngClassInvalid',
@ -884,6 +887,7 @@ var NG_ALL = [
'NgControlName.update=',
'NgControlName.valid',
'NgControlName.validator',
'NgControlName.asyncValidator',
'NgControlName.value',
'NgControlName.valueAccessor',
'NgControlName.valueAccessor=',
@ -941,6 +945,7 @@ var NG_ALL = [
'NgFormControl.update=',
'NgFormControl.valid',
'NgFormControl.validator',
'NgFormControl.asyncValidator',
'NgFormControl.value',
'NgFormControl.valueAccessor',
'NgFormControl.valueAccessor=',
@ -996,6 +1001,7 @@ var NG_ALL = [
'NgModel.update=',
'NgModel.valid',
'NgModel.validator',
'NgModel.asyncValidator',
'NgModel.value',
'NgModel.valueAccessor',
'NgModel.valueAccessor=',
@ -1223,6 +1229,7 @@ var NG_ALL = [
'UrlResolver',
'UrlResolver.resolve()',
'Validators#compose()',
'Validators#composeAsync()',
'Validators#nullValidator()',
'Validators#required()',
'Validators#minLength()',