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:
@ -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";
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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()',
|
||||
|
Reference in New Issue
Block a user