From 758062807aed3214b79758b7f2b69968a5b61b16 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 27 Oct 2015 14:36:13 -0700 Subject: [PATCH] feat(forms): support adding validators to ControlGroup via template Closes #4954 --- .../directives/abstract_control_directive.ts | 2 + .../src/core/forms/directives/ng_control.ts | 1 - .../core/forms/directives/ng_control_group.ts | 14 ++++- .../src/core/forms/directives/ng_form.ts | 13 +++- .../core/forms/directives/ng_form_model.ts | 29 +++++++-- .../src/core/forms/directives/shared.ts | 11 +++- .../test/core/forms/directives_spec.ts | 63 ++++++++++++++++--- .../test/core/forms/integration_spec.ts | 30 +++++++-- modules/angular2/test/public_api_spec.ts | 4 +- 9 files changed, 137 insertions(+), 30 deletions(-) diff --git a/modules/angular2/src/core/forms/directives/abstract_control_directive.ts b/modules/angular2/src/core/forms/directives/abstract_control_directive.ts index 94a7afeaa4..235816ebac 100644 --- a/modules/angular2/src/core/forms/directives/abstract_control_directive.ts +++ b/modules/angular2/src/core/forms/directives/abstract_control_directive.ts @@ -21,4 +21,6 @@ export class AbstractControlDirective { get touched(): boolean { return isPresent(this.control) ? this.control.touched : null; } get untouched(): boolean { return isPresent(this.control) ? this.control.untouched : null; } + + get path(): string[] { return null; } } diff --git a/modules/angular2/src/core/forms/directives/ng_control.ts b/modules/angular2/src/core/forms/directives/ng_control.ts index 462f5f36cc..f33b77aa12 100644 --- a/modules/angular2/src/core/forms/directives/ng_control.ts +++ b/modules/angular2/src/core/forms/directives/ng_control.ts @@ -14,7 +14,6 @@ export class NgControl extends AbstractControlDirective { valueAccessor: ControlValueAccessor = null; get validator(): Function { return null; } - get path(): string[] { return null; } viewToModelUpdate(newValue: any): void {} } diff --git a/modules/angular2/src/core/forms/directives/ng_control_group.ts b/modules/angular2/src/core/forms/directives/ng_control_group.ts index 03c52a224a..cec893902d 100644 --- a/modules/angular2/src/core/forms/directives/ng_control_group.ts +++ b/modules/angular2/src/core/forms/directives/ng_control_group.ts @@ -1,6 +1,6 @@ import {OnInit, OnDestroy} from 'angular2/lifecycle_hooks'; 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 {CONST_EXPR} from 'angular2/src/core/facade/lang'; @@ -8,6 +8,7 @@ import {ControlContainer} from './control_container'; import {controlPath} from './shared'; import {ControlGroup} from '../model'; import {Form} from './form_interface'; +import {Validators, NG_VALIDATORS} from '../validators'; const controlGroupBinding = CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgControlGroup)})); @@ -60,9 +61,14 @@ export class NgControlGroup extends ControlContainer implements OnInit, OnDestroy { /** @internal */ _parent: ControlContainer; - constructor(@Host() @SkipSelf() _parent: ControlContainer) { + + private _validators: Function[]; + + constructor(@Host() @SkipSelf() parent: ControlContainer, + @Optional() @Inject(NG_VALIDATORS) validators: Function[]) { super(); - this._parent = _parent; + this._parent = parent; + this._validators = validators; } 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 formDirective(): Form { return this._parent.formDirective; } + + get validator(): Function { return Validators.compose(this._validators); } } diff --git a/modules/angular2/src/core/forms/directives/ng_form.ts b/modules/angular2/src/core/forms/directives/ng_form.ts index 62ac673abf..3ba5d5a74a 100644 --- a/modules/angular2/src/core/forms/directives/ng_form.ts +++ b/modules/angular2/src/core/forms/directives/ng_form.ts @@ -7,13 +7,14 @@ import { import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection'; import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang'; 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 {Form} from './form_interface'; import {NgControlGroup} from './ng_control_group'; import {ControlContainer} from './control_container'; import {AbstractControl, ControlGroup, Control} from '../model'; -import {setUpControl} from './shared'; +import {setUpControl, setUpControlGroup} from './shared'; +import {Validators, NG_VALIDATORS} from '../validators'; const formDirectiveProvider = CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgForm)})); @@ -87,9 +88,14 @@ const formDirectiveProvider = exportAs: 'form' }) export class NgForm extends ControlContainer implements Form { - form: ControlGroup = new ControlGroup({}); + form: ControlGroup; 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 control(): ControlGroup { return this.form; } @@ -124,6 +130,7 @@ export class NgForm extends ControlContainer implements Form { this._later(_ => { var container = this._findContainer(dir.path); var group = new ControlGroup({}); + setUpControlGroup(group, dir); container.addControl(dir.name, group); group.updateValueAndValidity({emitEvent: false}); }); diff --git a/modules/angular2/src/core/forms/directives/ng_form_model.ts b/modules/angular2/src/core/forms/directives/ng_form_model.ts index 5369118e30..ee376810f5 100644 --- a/modules/angular2/src/core/forms/directives/ng_form_model.ts +++ b/modules/angular2/src/core/forms/directives/ng_form_model.ts @@ -1,16 +1,18 @@ 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 {SimpleChange} from 'angular2/src/core/change_detection'; import {OnChanges} from 'angular2/lifecycle_hooks'; 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 {NgControlGroup} from './ng_control_group'; import {ControlContainer} from './control_container'; import {Form} from './form_interface'; import {Control, ControlGroup} from '../model'; -import {setUpControl} from './shared'; +import {setUpControl, setUpControlGroup} from './shared'; +import {Validators, NG_VALIDATORS} from '../validators'; const formDirectiveProvider = CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgFormModel)})); @@ -100,8 +102,21 @@ export class NgFormModel extends ControlContainer implements Form, form: ControlGroup = null; directives: NgControl[] = []; 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; } @@ -120,7 +135,11 @@ export class NgFormModel extends ControlContainer implements Form, 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) {} diff --git a/modules/angular2/src/core/forms/directives/shared.ts b/modules/angular2/src/core/forms/directives/shared.ts index 127dea54d6..e325a6e9ab 100644 --- a/modules/angular2/src/core/forms/directives/shared.ts +++ b/modules/angular2/src/core/forms/directives/shared.ts @@ -4,7 +4,9 @@ import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptio import {ControlContainer} from './control_container'; 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 {ControlValueAccessor} from './control_value_accessor'; 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()); } -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(" -> "); throw new BaseException(`${message} '${path}'`); } diff --git a/modules/angular2/test/core/forms/directives_spec.ts b/modules/angular2/test/core/forms/directives_spec.ts index 520d616926..2d855adaa0 100644 --- a/modules/angular2/test/core/forms/directives_spec.ts +++ b/modules/angular2/test/core/forms/directives_spec.ts @@ -105,8 +105,12 @@ export function main() { var loginControlDir; beforeEach(() => { - form = new NgFormModel(); - formModel = new ControlGroup({"login": new Control(null)}); + form = new NgFormModel([]); + formModel = new ControlGroup({ + "login": new Control(), + "passwords": + new ControlGroup({"password": new Control(), "passwordConfirm": new Control()}) + }); form.form = formModel; 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", () => { it("should remove the directive to the list of directives included in the form", () => { form.addControl(loginControlDir); @@ -181,10 +205,22 @@ export function main() { formModel.find(["login"]).updateValue("new value"); - form.onChanges(null); + form.onChanges({}); expect((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; 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]); @@ -246,6 +282,17 @@ export function main() { // 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", () => { @@ -255,9 +302,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"; }); @@ -356,7 +403,7 @@ 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.name = "name"; diff --git a/modules/angular2/test/core/forms/integration_spec.ts b/modules/angular2/test/core/forms/integration_spec.ts index 9b4e4f6d8e..8a4bd4e84e 100644 --- a/modules/angular2/test/core/forms/integration_spec.ts +++ b/modules/angular2/test/core/forms/integration_spec.ts @@ -24,6 +24,8 @@ import { ControlGroup, ControlValueAccessor, FORM_DIRECTIVES, + NG_VALIDATORS, + Provider, NgControl, NgIf, NgFor, @@ -398,10 +400,10 @@ export function main() { var form = new ControlGroup( {"login": new Control(""), "min": new Control(""), "max": new Control("")}); - var t = `
- - - + var t = `
+ + +
`; 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("maxlength", ["max"])).toEqual(true); + expect(form.hasError("loginIsEmpty")).toEqual(true); + required.nativeElement.value = "1"; minLength.nativeElement.value = "123"; maxLength.nativeElement.value = "123"; @@ -914,8 +918,22 @@ class MyInput implements ControlValueAccessor { } } -@Component({selector: "my-comp"}) -@View({directives: [FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor]}) +function loginIsEmptyGroupValidator(c: ControlGroup) { + 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 { form: any; name: string; diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 7b7d3db164..0f628ab78c 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -73,6 +73,7 @@ var NG_API = [ 'AbstractControlDirective.untouched', 'AbstractControlDirective.valid', 'AbstractControlDirective.value', + 'AbstractControlDirective.path', 'AppRootUrl', 'AppRootUrl.value', 'AppRootUrl.value=', @@ -657,6 +658,7 @@ var NG_API = [ 'NgControlGroup.untouched', 'NgControlGroup.valid', 'NgControlGroup.value', + 'NgControlGroup.validator', 'NgControlStatus', 'NgControlStatus.ngClassDirty', 'NgControlStatus.ngClassInvalid', @@ -1030,9 +1032,7 @@ var NG_API = [ 'UpperCasePipe.transform()', 'UrlResolver', 'UrlResolver.resolve()', - 'Validators#array()', 'Validators#compose()', - 'Validators#group()', 'Validators#nullValidator()', 'Validators#required()', 'Validators#minLength()',