feat(forms): support adding validators to ControlGroup via template

Closes #4954
This commit is contained in:
vsavkin 2015-10-27 14:36:13 -07:00 committed by Victor Savkin
parent f98faf0702
commit 758062807a
9 changed files with 137 additions and 30 deletions

View File

@ -21,4 +21,6 @@ export class AbstractControlDirective {
get touched(): boolean { return isPresent(this.control) ? this.control.touched : null; } get touched(): boolean { return isPresent(this.control) ? this.control.touched : null; }
get untouched(): boolean { return isPresent(this.control) ? this.control.untouched : null; } get untouched(): boolean { return isPresent(this.control) ? this.control.untouched : null; }
get path(): string[] { return null; }
} }

View File

@ -14,7 +14,6 @@ export class NgControl extends AbstractControlDirective {
valueAccessor: ControlValueAccessor = null; valueAccessor: ControlValueAccessor = null;
get validator(): Function { return null; } get validator(): Function { return null; }
get path(): string[] { return null; }
viewToModelUpdate(newValue: any): void {} viewToModelUpdate(newValue: any): void {}
} }

View File

@ -1,6 +1,6 @@
import {OnInit, OnDestroy} from 'angular2/lifecycle_hooks'; import {OnInit, OnDestroy} from 'angular2/lifecycle_hooks';
import {Directive} from 'angular2/src/core/metadata'; import {Directive} from 'angular2/src/core/metadata';
import {Inject, Host, SkipSelf, forwardRef, Provider} from 'angular2/src/core/di'; import {Optional, Inject, Host, SkipSelf, forwardRef, Provider} from 'angular2/src/core/di';
import {ListWrapper} from 'angular2/src/core/facade/collection'; import {ListWrapper} from 'angular2/src/core/facade/collection';
import {CONST_EXPR} from 'angular2/src/core/facade/lang'; import {CONST_EXPR} from 'angular2/src/core/facade/lang';
@ -8,6 +8,7 @@ import {ControlContainer} from './control_container';
import {controlPath} from './shared'; import {controlPath} from './shared';
import {ControlGroup} from '../model'; import {ControlGroup} from '../model';
import {Form} from './form_interface'; import {Form} from './form_interface';
import {Validators, NG_VALIDATORS} from '../validators';
const controlGroupBinding = const controlGroupBinding =
CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgControlGroup)})); CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgControlGroup)}));
@ -60,9 +61,14 @@ export class NgControlGroup extends ControlContainer implements OnInit,
OnDestroy { OnDestroy {
/** @internal */ /** @internal */
_parent: ControlContainer; _parent: ControlContainer;
constructor(@Host() @SkipSelf() _parent: ControlContainer) {
private _validators: Function[];
constructor(@Host() @SkipSelf() parent: ControlContainer,
@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
super(); super();
this._parent = _parent; this._parent = parent;
this._validators = validators;
} }
onInit(): void { this.formDirective.addControlGroup(this); } onInit(): void { this.formDirective.addControlGroup(this); }
@ -74,4 +80,6 @@ export class NgControlGroup extends ControlContainer implements OnInit,
get path(): string[] { return controlPath(this.name, this._parent); } get path(): string[] { return controlPath(this.name, this._parent); }
get formDirective(): Form { return this._parent.formDirective; } get formDirective(): Form { return this._parent.formDirective; }
get validator(): Function { return Validators.compose(this._validators); }
} }

View File

@ -7,13 +7,14 @@ import {
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection'; import {StringMapWrapper, ListWrapper} 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 {Directive} from 'angular2/src/core/metadata'; import {Directive} from 'angular2/src/core/metadata';
import {forwardRef, Provider} from 'angular2/src/core/di'; import {forwardRef, Provider, Optional, Inject} from 'angular2/src/core/di';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {Form} from './form_interface'; import {Form} from './form_interface';
import {NgControlGroup} from './ng_control_group'; import {NgControlGroup} from './ng_control_group';
import {ControlContainer} from './control_container'; import {ControlContainer} from './control_container';
import {AbstractControl, ControlGroup, Control} from '../model'; import {AbstractControl, ControlGroup, Control} from '../model';
import {setUpControl} from './shared'; import {setUpControl, setUpControlGroup} from './shared';
import {Validators, NG_VALIDATORS} from '../validators';
const formDirectiveProvider = const formDirectiveProvider =
CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgForm)})); CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgForm)}));
@ -87,9 +88,14 @@ const formDirectiveProvider =
exportAs: 'form' exportAs: 'form'
}) })
export class NgForm extends ControlContainer implements Form { export class NgForm extends ControlContainer implements Form {
form: ControlGroup = new ControlGroup({}); form: ControlGroup;
ngSubmit = new EventEmitter(); ngSubmit = new EventEmitter();
constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
super();
this.form = new ControlGroup({}, null, Validators.compose(validators));
}
get formDirective(): Form { return this; } get formDirective(): Form { return this; }
get control(): ControlGroup { return this.form; } get control(): ControlGroup { return this.form; }
@ -124,6 +130,7 @@ export class NgForm extends ControlContainer implements Form {
this._later(_ => { this._later(_ => {
var container = this._findContainer(dir.path); var container = this._findContainer(dir.path);
var group = new ControlGroup({}); var group = new ControlGroup({});
setUpControlGroup(group, dir);
container.addControl(dir.name, group); container.addControl(dir.name, group);
group.updateValueAndValidity({emitEvent: false}); group.updateValueAndValidity({emitEvent: false});
}); });

View File

@ -1,16 +1,18 @@
import {CONST_EXPR} from 'angular2/src/core/facade/lang'; import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {ListWrapper} from 'angular2/src/core/facade/collection'; import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async'; import {ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async';
import {SimpleChange} from 'angular2/src/core/change_detection';
import {OnChanges} from 'angular2/lifecycle_hooks'; import {OnChanges} from 'angular2/lifecycle_hooks';
import {Directive} from 'angular2/src/core/metadata'; import {Directive} from 'angular2/src/core/metadata';
import {forwardRef, Provider} from 'angular2/src/core/di'; import {forwardRef, Provider, Inject, Optional} from 'angular2/src/core/di';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {NgControlGroup} from './ng_control_group'; import {NgControlGroup} from './ng_control_group';
import {ControlContainer} from './control_container'; import {ControlContainer} from './control_container';
import {Form} from './form_interface'; import {Form} from './form_interface';
import {Control, ControlGroup} from '../model'; import {Control, ControlGroup} from '../model';
import {setUpControl} from './shared'; import {setUpControl, setUpControlGroup} from './shared';
import {Validators, NG_VALIDATORS} from '../validators';
const formDirectiveProvider = const formDirectiveProvider =
CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgFormModel)})); CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgFormModel)}));
@ -100,8 +102,21 @@ export class NgFormModel extends ControlContainer implements Form,
form: ControlGroup = null; form: ControlGroup = null;
directives: NgControl[] = []; directives: NgControl[] = [];
ngSubmit = new EventEmitter(); ngSubmit = new EventEmitter();
private _validators: Function[];
onChanges(_): void { this._updateDomValue(); } constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
super();
this._validators = validators;
}
onChanges(changes: {[key: string]: SimpleChange}): void {
if (StringMapWrapper.contains(changes, "form")) {
var c = Validators.compose(this._validators);
this.form.validator = Validators.compose([this.form.validator, c]);
}
this._updateDomValue();
}
get formDirective(): Form { return this; } get formDirective(): Form { return this; }
@ -120,7 +135,11 @@ export class NgFormModel extends ControlContainer implements Form,
removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); } removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); }
addControlGroup(dir: NgControlGroup) {} addControlGroup(dir: NgControlGroup) {
var ctrl: any = this.form.find(dir.path);
setUpControlGroup(ctrl, dir);
ctrl.updateValueAndValidity({emitEvent: false});
}
removeControlGroup(dir: NgControlGroup) {} removeControlGroup(dir: NgControlGroup) {}

View File

@ -4,7 +4,9 @@ import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptio
import {ControlContainer} from './control_container'; import {ControlContainer} from './control_container';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {Control} from '../model'; import {AbstractControlDirective} from './abstract_control_directive';
import {NgControlGroup} from './ng_control_group';
import {Control, ControlGroup} from '../model';
import {Validators} from '../validators'; import {Validators} from '../validators';
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
import {ElementRef, QueryList} from 'angular2/src/core/linker'; import {ElementRef, QueryList} from 'angular2/src/core/linker';
@ -42,7 +44,12 @@ export function setUpControl(control: Control, dir: NgControl): void {
dir.valueAccessor.registerOnTouched(() => control.markAsTouched()); dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
} }
function _throwError(dir: NgControl, message: string): void { export function setUpControlGroup(control: ControlGroup, dir: NgControlGroup) {
if (isBlank(control)) _throwError(dir, "Cannot find control");
control.validator = Validators.compose([control.validator, dir.validator]);
}
function _throwError(dir: AbstractControlDirective, message: string): void {
var path = dir.path.join(" -> "); var path = dir.path.join(" -> ");
throw new BaseException(`${message} '${path}'`); throw new BaseException(`${message} '${path}'`);
} }

View File

@ -105,8 +105,12 @@ export function main() {
var loginControlDir; var loginControlDir;
beforeEach(() => { beforeEach(() => {
form = new NgFormModel(); form = new NgFormModel([]);
formModel = new ControlGroup({"login": new Control(null)}); formModel = new ControlGroup({
"login": new Control(),
"passwords":
new ControlGroup({"password": new Control(), "passwordConfirm": new Control()})
});
form.form = formModel; form.form = formModel;
loginControlDir = new NgControlName(form, [], [defaultAccessor]); loginControlDir = new NgControlName(form, [], [defaultAccessor]);
@ -167,6 +171,26 @@ export function main() {
}); });
}); });
describe("addControlGroup", () => {
var matchingPasswordsValidator = (g) => {
if (g.controls["password"].value != g.controls["passwordConfirm"].value) {
return {"differentPasswords": true};
} else {
return null;
}
};
it("should set up validator", () => {
var group = new NgControlGroup(form, [matchingPasswordsValidator]);
group.name = "passwords";
form.addControlGroup(group);
formModel.find(["passwords", "password"]).updateValue("somePassword");
expect(formModel.hasError("differentPasswords", ["passwords"])).toEqual(true);
});
});
describe("removeControl", () => { describe("removeControl", () => {
it("should remove the directive to the list of directives included in the form", () => { it("should remove the directive to the list of directives included in the form", () => {
form.addControl(loginControlDir); form.addControl(loginControlDir);
@ -181,10 +205,22 @@ export function main() {
formModel.find(["login"]).updateValue("new value"); formModel.find(["login"]).updateValue("new value");
form.onChanges(null); form.onChanges({});
expect((<any>loginControlDir.valueAccessor).writtenValue).toEqual("new value"); expect((<any>loginControlDir.valueAccessor).writtenValue).toEqual("new value");
}); });
it("should set up validator", () => {
var formValidator = (c) => ({"custom": true});
var f = new NgFormModel([formValidator]);
f.form = formModel;
f.onChanges({"form": formModel});
// trigger validation
formModel.controls["login"].updateValue("");
expect(formModel.errors).toEqual({"custom": true});
});
}); });
}); });
@ -195,10 +231,10 @@ export function main() {
var personControlGroupDir; var personControlGroupDir;
beforeEach(() => { beforeEach(() => {
form = new NgForm(); form = new NgForm([]);
formModel = form.form; formModel = form.form;
personControlGroupDir = new NgControlGroup(form); personControlGroupDir = new NgControlGroup(form, []);
personControlGroupDir.name = "person"; personControlGroupDir.name = "person";
loginControlDir = new NgControlName(personControlGroupDir, null, [defaultAccessor]); loginControlDir = new NgControlName(personControlGroupDir, null, [defaultAccessor]);
@ -246,6 +282,17 @@ export function main() {
// should update the form's value and validity // should update the form's value and validity
}); });
it("should set up validator", fakeAsync(() => {
var formValidator = (c) => ({"custom": true});
var f = new NgForm([formValidator]);
f.addControlGroup(personControlGroupDir);
f.addControl(loginControlDir);
flushMicrotasks();
expect(f.form.errors).toEqual({"custom": true});
}));
}); });
describe("NgControlGroup", () => { describe("NgControlGroup", () => {
@ -255,9 +302,9 @@ export function main() {
beforeEach(() => { beforeEach(() => {
formModel = new ControlGroup({"login": new Control(null)}); formModel = new ControlGroup({"login": new Control(null)});
var parent = new NgFormModel(); var parent = new NgFormModel([]);
parent.form = new ControlGroup({"group": formModel}); parent.form = new ControlGroup({"group": formModel});
controlGroupDir = new NgControlGroup(parent); controlGroupDir = new NgControlGroup(parent, []);
controlGroupDir.name = "group"; controlGroupDir.name = "group";
}); });
@ -356,7 +403,7 @@ export function main() {
beforeEach(() => { beforeEach(() => {
formModel = new Control("name"); formModel = new Control("name");
var parent = new NgFormModel(); var parent = new NgFormModel([]);
parent.form = new ControlGroup({"name": formModel}); parent.form = new ControlGroup({"name": formModel});
controlNameDir = new NgControlName(parent, [], [defaultAccessor]); controlNameDir = new NgControlName(parent, [], [defaultAccessor]);
controlNameDir.name = "name"; controlNameDir.name = "name";

View File

@ -24,6 +24,8 @@ import {
ControlGroup, ControlGroup,
ControlValueAccessor, ControlValueAccessor,
FORM_DIRECTIVES, FORM_DIRECTIVES,
NG_VALIDATORS,
Provider,
NgControl, NgControl,
NgIf, NgIf,
NgFor, NgFor,
@ -398,10 +400,10 @@ export function main() {
var form = new ControlGroup( var form = new ControlGroup(
{"login": new Control(""), "min": new Control(""), "max": new Control("")}); {"login": new Control(""), "min": new Control(""), "max": new Control("")});
var t = `<div [ng-form-model]="form"> var t = `<div [ng-form-model]="form" login-is-empty-validator>
<input type="text" ng-control="login" required> <input type="text" ng-control="login" required>
<input type="text" ng-control="min" minlength="3"> <input type="text" ng-control="min" minlength="3">
<input type="text" ng-control="max" maxlength="3"> <input type="text" ng-control="max" maxlength="3">
</div>`; </div>`;
tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((rootTC) => { tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((rootTC) => {
@ -423,6 +425,8 @@ export function main() {
expect(form.hasError("minlength", ["min"])).toEqual(true); expect(form.hasError("minlength", ["min"])).toEqual(true);
expect(form.hasError("maxlength", ["max"])).toEqual(true); expect(form.hasError("maxlength", ["max"])).toEqual(true);
expect(form.hasError("loginIsEmpty")).toEqual(true);
required.nativeElement.value = "1"; required.nativeElement.value = "1";
minLength.nativeElement.value = "123"; minLength.nativeElement.value = "123";
maxLength.nativeElement.value = "123"; maxLength.nativeElement.value = "123";
@ -914,8 +918,22 @@ class MyInput implements ControlValueAccessor {
} }
} }
@Component({selector: "my-comp"}) function loginIsEmptyGroupValidator(c: ControlGroup) {
@View({directives: [FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor]}) return c.controls["login"].value == "" ? {"loginIsEmpty": true} : null;
}
@Directive({
selector: '[login-is-empty-validator]',
providers: [new Provider(NG_VALIDATORS, {useValue: loginIsEmptyGroupValidator, multi: true})]
})
class LoginIsEmptyValidator {
}
@Component({
selector: "my-comp",
template: '',
directives: [FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator]
})
class MyComp { class MyComp {
form: any; form: any;
name: string; name: string;

View File

@ -73,6 +73,7 @@ var NG_API = [
'AbstractControlDirective.untouched', 'AbstractControlDirective.untouched',
'AbstractControlDirective.valid', 'AbstractControlDirective.valid',
'AbstractControlDirective.value', 'AbstractControlDirective.value',
'AbstractControlDirective.path',
'AppRootUrl', 'AppRootUrl',
'AppRootUrl.value', 'AppRootUrl.value',
'AppRootUrl.value=', 'AppRootUrl.value=',
@ -657,6 +658,7 @@ var NG_API = [
'NgControlGroup.untouched', 'NgControlGroup.untouched',
'NgControlGroup.valid', 'NgControlGroup.valid',
'NgControlGroup.value', 'NgControlGroup.value',
'NgControlGroup.validator',
'NgControlStatus', 'NgControlStatus',
'NgControlStatus.ngClassDirty', 'NgControlStatus.ngClassDirty',
'NgControlStatus.ngClassInvalid', 'NgControlStatus.ngClassInvalid',
@ -1030,9 +1032,7 @@ var NG_API = [
'UpperCasePipe.transform()', 'UpperCasePipe.transform()',
'UrlResolver', 'UrlResolver',
'UrlResolver.resolve()', 'UrlResolver.resolve()',
'Validators#array()',
'Validators#compose()', 'Validators#compose()',
'Validators#group()',
'Validators#nullValidator()', 'Validators#nullValidator()',
'Validators#required()', 'Validators#required()',
'Validators#minLength()', 'Validators#minLength()',