From 733915d99bb6f112d5217eaa42ca9273af8413b9 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 25 Feb 2015 15:10:27 -0800 Subject: [PATCH] feat(forms): add support for nested forms --- modules/angular2/src/forms/directives.js | 43 +++- modules/angular2/src/forms/model.js | 69 ++--- .../angular2/test/forms/integration_spec.js | 48 ++++ modules/angular2/test/forms/model_spec.js | 238 ++++++++++-------- 4 files changed, 234 insertions(+), 164 deletions(-) diff --git a/modules/angular2/src/forms/directives.js b/modules/angular2/src/forms/directives.js index b833937f91..f217177466 100644 --- a/modules/angular2/src/forms/directives.js +++ b/modules/angular2/src/forms/directives.js @@ -1,6 +1,7 @@ import {Template, Component, Decorator, NgElement, Ancestor, onChange} from 'angular2/core'; +import {Optional} from 'angular2/di'; import {DOM} from 'angular2/src/dom/dom_adapter'; -import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang'; +import {isBlank, isPresent, isString, CONST} from 'angular2/src/facade/lang'; import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import {ControlGroup, Control} from './model'; import * as validators from './validators'; @@ -64,7 +65,7 @@ function controlValueAccessorFor(controlType:string):ControlValueAccessor { } }) export class ControlDirective { - _groupDecorator:ControlGroupDirective; + _groupDirective:ControlGroupDirective; _el:NgElement; controlName:string; @@ -73,8 +74,8 @@ export class ControlDirective { validator:Function; - constructor(@Ancestor() groupDecorator:ControlGroupDirective, el:NgElement) { - this._groupDecorator = groupDecorator; + constructor(@Ancestor() groupDirective:ControlGroupDirective, el:NgElement) { + this._groupDirective = groupDirective; this._el = el; this.validator = validators.nullValidator; } @@ -86,7 +87,7 @@ export class ControlDirective { } _initialize() { - this._groupDecorator.addDirective(this); + this._groupDirective.addDirective(this); var c = this._control(); c.validator = validators.compose([c.validator, this.validator]); @@ -108,7 +109,7 @@ export class ControlDirective { } _control() { - return this._groupDecorator.findControl(this.controlName); + return this._groupDirective.findControl(this.controlName); } } @@ -119,16 +120,28 @@ export class ControlDirective { } }) export class ControlGroupDirective { + _groupDirective:ControlGroupDirective; + _controlGroupName:string; + _controlGroup:ControlGroup; _directives:List; - constructor() { + constructor(@Optional() @Ancestor() groupDirective:ControlGroupDirective) { super(); + this._groupDirective = groupDirective; this._directives = ListWrapper.create(); } - set controlGroup(controlGroup:ControlGroup) { - this._controlGroup = controlGroup; + set controlGroup(controlGroup) { + if (isString(controlGroup)) { + this._controlGroupName = controlGroup; + } else { + this._controlGroup = controlGroup; + } + this._updateDomValue(); + } + + _updateDomValue() { ListWrapper.forEach(this._directives, (cd) => cd._updateDomValue()); } @@ -136,8 +149,16 @@ export class ControlGroupDirective { ListWrapper.push(this._directives, c); } - findControl(name:string):Control { - return this._controlGroup.controls[name]; + findControl(name:string):any { + return this._getControlGroup().controls[name]; + } + + _getControlGroup():ControlGroup { + if (isPresent(this._controlGroupName)) { + return this._groupDirective.findControl(this._controlGroupName) + } else { + return this._controlGroup; + } } } diff --git a/modules/angular2/src/forms/model.js b/modules/angular2/src/forms/model.js index 11212e9be4..e6eba381da 100644 --- a/modules/angular2/src/forms/model.js +++ b/modules/angular2/src/forms/model.js @@ -16,7 +16,7 @@ export const INVALID = "INVALID"; // setParent(parent){} //} -export class Control { +export class AbstractControl { _value:any; _status:string; _errors; @@ -24,23 +24,17 @@ export class Control { _parent:ControlGroup; validator:Function; - constructor(value:any, validator:Function = nullValidator) { - this._value = value; + constructor(validator:Function = nullValidator) { this.validator = validator; this._dirty = true; } - updateValue(value:any) { - this._value = value; - this._dirty = true; - this._updateParent(); - } - get active():boolean { return true; } get value() { + this._updateIfNeeded(); return this._value; } @@ -64,11 +58,6 @@ export class Control { } _updateIfNeeded() { - if (this._dirty) { - this._dirty = false; - this._errors = this.validator(this); - this._status = isPresent(this._errors) ? INVALID : VALID; - } } _updateParent() { @@ -78,41 +67,36 @@ export class Control { } } -export class ControlGroup { - _value:any; - _status:string; - _errors; - _dirty:boolean; - validator:Function; +export class Control extends AbstractControl { + constructor(value:any, validator:Function = nullValidator) { + super(validator); + this._value = value; + } + + updateValue(value:any) { + this._value = value; + this._dirty = true; + this._updateParent(); + } + + _updateIfNeeded() { + if (this._dirty) { + this._dirty = false; + this._errors = this.validator(this); + this._status = isPresent(this._errors) ? INVALID : VALID; + } + } +} + +export class ControlGroup extends AbstractControl { controls; constructor(controls, validator:Function = controlGroupValidator) { + super(validator); this.controls = controls; - this.validator = validator; - this._dirty = true; this._setParentForControls(); } - get value() { - this._updateIfNeeded(); - return this._value; - } - - get status() { - this._updateIfNeeded(); - return this._status; - } - - get valid() { - this._updateIfNeeded(); - return this._status === VALID; - } - - get errors() { - this._updateIfNeeded(); - return this._errors; - } - _setParentForControls() { StringMapWrapper.forEach(this.controls, (control, name) => { control.setParent(this); @@ -140,6 +124,7 @@ export class ControlGroup { _controlChanged() { this._dirty = true; + this._updateParent(); } } diff --git a/modules/angular2/test/forms/integration_spec.js b/modules/angular2/test/forms/integration_spec.js index 27eca06218..745be3c2e2 100644 --- a/modules/angular2/test/forms/integration_spec.js +++ b/modules/angular2/test/forms/integration_spec.js @@ -218,6 +218,54 @@ export function main() { }); }); }); + + describe("nested forms", () => { + it("should init DOM with the given form object", (done) => { + var form = new ControlGroup({ + "nested": new ControlGroup({ + "login": new Control("value") + }) + }); + var ctx = new MyComp(form); + + var t = `
+
+ +
+
`; + + compile(MyComp, t, ctx, (view) => { + var input = queryView(view, "input") + expect(input.value).toEqual("value"); + done(); + }); + }); + + it("should update the control group values on DOM change", (done) => { + var form = new ControlGroup({ + "nested": new ControlGroup({ + "login": new Control("value") + }) + }); + var ctx = new MyComp(form); + + var t = `
+
+ +
+
`; + + compile(MyComp, t, ctx, (view) => { + var input = queryView(view, "input") + + input.value = "updatedValue"; + dispatchEvent(input, "change"); + + expect(form.value).toEqual({"nested" : {"login" : "updatedValue"}}); + done(); + }); + }); + }); }); } diff --git a/modules/angular2/test/forms/model_spec.js b/modules/angular2/test/forms/model_spec.js index fa24809a84..65e405e8f9 100644 --- a/modules/angular2/test/forms/model_spec.js +++ b/modules/angular2/test/forms/model_spec.js @@ -3,121 +3,137 @@ import {ControlGroup, Control, OptionalControl} from 'angular2/forms'; import * as validations from 'angular2/forms'; export function main() { - describe("Control", () => { - describe("validator", () => { - it("should run validator with the initial value", () => { - var c = new Control("value", validations.required); - expect(c.valid).toEqual(true); - }); - - it("should rerun the validator when the value changes", () => { - var c = new Control("value", validations.required); - c.updateValue(null); - expect(c.valid).toEqual(false); - }); - - it("should return errors", () => { - var c = new Control(null, validations.required); - expect(c.errors).toEqual({"required" : true}); - }); - }); - }); - - describe("ControlGroup", () => { - describe("value", () => { - it("should be the reduced value of the child controls", () => { - var g = new ControlGroup({ - "one": new Control("111"), - "two": new Control("222") - }); - expect(g.value).toEqual({"one": "111", "two": "222"}) - }); - - it("should be empty when there are no child controls", () => { - var g = new ControlGroup({}); - expect(g.value).toEqual({}) - }); - }); - - describe("validator", () => { - it("should run the validator with the initial value (valid)", () => { - var g = new ControlGroup({ - "one": new Control('value', validations.required) + describe("Form Model", () => { + describe("Control", () => { + describe("validator", () => { + it("should run validator with the initial value", () => { + var c = new Control("value", validations.required); + expect(c.valid).toEqual(true); }); - expect(g.valid).toEqual(true); + it("should rerun the validator when the value changes", () => { + var c = new Control("value", validations.required); + c.updateValue(null); + expect(c.valid).toEqual(false); + }); - expect(g.errors).toEqual(null); - }); - - it("should run the validator with the initial value (invalid)", () => { - var one = new Control(null, validations.required); - var g = new ControlGroup({"one": one}); - - expect(g.valid).toEqual(false); - - expect(g.errors).toEqual({"required": [one]}); - }); - - it("should run the validator with the value changes", () => { - var c = new Control(null, validations.required); - var g = new ControlGroup({"one": c}); - - c.updateValue("some value"); - - expect(g.valid).toEqual(true); - expect(g.errors).toEqual(null); + it("should return errors", () => { + var c = new Control(null, validations.required); + expect(c.errors).toEqual({"required" : true}); + }); }); }); + + describe("ControlGroup", () => { + describe("value", () => { + it("should be the reduced value of the child controls", () => { + var g = new ControlGroup({ + "one": new Control("111"), + "two": new Control("222") + }); + expect(g.value).toEqual({"one": "111", "two": "222"}); + }); + + it("should be empty when there are no child controls", () => { + var g = new ControlGroup({}); + expect(g.value).toEqual({}); + }); + + it("should support nested groups", () => { + var g = new ControlGroup({ + "one": new Control("111"), + "nested": new ControlGroup({ + "two" : new Control("222") + }) + }); + expect(g.value).toEqual({"one": "111", "nested" : {"two": "222"}}); + + g.controls["nested"].controls["two"].updateValue("333"); + + expect(g.value).toEqual({"one": "111", "nested" : {"two": "333"}}); + }); + }); + + describe("validator", () => { + it("should run the validator with the initial value (valid)", () => { + var g = new ControlGroup({ + "one": new Control('value', validations.required) + }); + + expect(g.valid).toEqual(true); + + expect(g.errors).toEqual(null); + }); + + it("should run the validator with the initial value (invalid)", () => { + var one = new Control(null, validations.required); + var g = new ControlGroup({"one": one}); + + expect(g.valid).toEqual(false); + + expect(g.errors).toEqual({"required": [one]}); + }); + + it("should run the validator with the value changes", () => { + var c = new Control(null, validations.required); + var g = new ControlGroup({"one": c}); + + c.updateValue("some value"); + + expect(g.valid).toEqual(true); + expect(g.errors).toEqual(null); + }); + }); + }); + + describe("OptionalControl", () => { + it("should read properties from the wrapped component", () => { + var wrapperControl = new Control("value", validations.required); + var c = new OptionalControl(wrapperControl, true); + + expect(c.value).toEqual('value'); + expect(c.status).toEqual('VALID'); + expect(c.validator).toEqual(validations.required); + }); + + it("should update the wrapped component", () => { + var wrappedControl = new Control("value"); + var c = new OptionalControl(wrappedControl, true); + + c.validator = validations.required; + c.updateValue("newValue"); + + + expect(wrappedControl.validator).toEqual(validations.required); + expect(wrappedControl.value).toEqual('newValue'); + }); + + it("should not include an inactive component into the group value", () => { + var group = new ControlGroup({ + "required" : new Control("requiredValue"), + "optional" : new OptionalControl(new Control("optionalValue"), false) + }); + + expect(group.value).toEqual({"required" : "requiredValue"}); + + group.controls["optional"].cond = true; + + expect(group.value).toEqual({"required" : "requiredValue", "optional" : "optionalValue"}); + }); + + it("should not run validations on an inactive component", () => { + var group = new ControlGroup({ + "required" : new Control("requiredValue", validations.required), + "optional" : new OptionalControl(new Control("", validations.required), false) + }); + + expect(group.valid).toEqual(true); + + group.controls["optional"].cond = true; + + expect(group.valid).toEqual(false); + }); + }); + }); - - describe("OptionalControl", () => { - it("should read properties from the wrapped component", () => { - var wrapperControl = new Control("value", validations.required); - var c = new OptionalControl(wrapperControl, true); - - expect(c.value).toEqual('value'); - expect(c.status).toEqual('VALID'); - expect(c.validator).toEqual(validations.required); - }); - - it("should update the wrapped component", () => { - var wrappedControl = new Control("value"); - var c = new OptionalControl(wrappedControl, true); - - c.validator = validations.required; - c.updateValue("newValue"); - - - expect(wrappedControl.validator).toEqual(validations.required); - expect(wrappedControl.value).toEqual('newValue'); - }); - - it("should not include an inactive component into the group value", () => { - var group = new ControlGroup({ - "required" : new Control("requiredValue"), - "optional" : new OptionalControl(new Control("optionalValue"), false) - }); - - expect(group.value).toEqual({"required" : "requiredValue"}); - - group.controls["optional"].cond = true; - - expect(group.value).toEqual({"required" : "requiredValue", "optional" : "optionalValue"}); - }); - - it("should not run validations on an inactive component", () => { - var group = new ControlGroup({ - "required" : new Control("requiredValue", validations.required), - "optional" : new OptionalControl(new Control("", validations.required), false) - }); - - expect(group.valid).toEqual(true); - - group.controls["optional"].cond = true; - - expect(group.valid).toEqual(false); - }); - }); - } \ No newline at end of file