From ff84506bd5fd2e50fa945a64104ef9379f1f3b69 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 25 Mar 2015 10:51:05 -0700 Subject: [PATCH] feat(forms): added support for arrays of controls --- modules/angular2/src/forms/form_builder.js | 15 +- modules/angular2/src/forms/model.js | 74 +++++- modules/angular2/src/forms/validators.js | 26 ++- .../angular2/test/forms/form_builder_spec.js | 15 +- modules/angular2/test/forms/model_spec.js | 214 +++++++++++++++--- 5 files changed, 297 insertions(+), 47 deletions(-) diff --git a/modules/angular2/src/forms/form_builder.js b/modules/angular2/src/forms/form_builder.js index 2180560ca0..9111c94bbe 100644 --- a/modules/angular2/src/forms/form_builder.js +++ b/modules/angular2/src/forms/form_builder.js @@ -1,4 +1,4 @@ -import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection'; +import {StringMapWrapper, ListWrapper, List} from 'angular2/src/facade/collection'; import {isPresent} from 'angular2/src/facade/lang'; import * as modelModule from './model'; @@ -24,6 +24,15 @@ export class FormBuilder { } } + array(controlsConfig:List, validator:Function = null):modelModule.ControlArray { + var controls = ListWrapper.map(controlsConfig, (c) => this._createControl(c)); + if (isPresent(validator)) { + return new modelModule.ControlArray(controls, validator); + } else { + return new modelModule.ControlArray(controls); + } + } + _reduceControls(controlsConfig) { var controls = {}; StringMapWrapper.forEach(controlsConfig, (controlConfig, controlName) => { @@ -33,7 +42,9 @@ export class FormBuilder { } _createControl(controlConfig) { - if (controlConfig instanceof modelModule.Control || controlConfig instanceof modelModule.ControlGroup) { + if (controlConfig instanceof modelModule.Control || + controlConfig instanceof modelModule.ControlGroup || + controlConfig instanceof modelModule.ControlArray) { return controlConfig; } else if (ListWrapper.isList(controlConfig)) { diff --git a/modules/angular2/src/forms/model.js b/modules/angular2/src/forms/model.js index 62698ef371..7db069a1a4 100644 --- a/modules/angular2/src/forms/model.js +++ b/modules/angular2/src/forms/model.js @@ -1,6 +1,6 @@ import {isPresent} from 'angular2/src/facade/lang'; import {Observable, ObservableWrapper} from 'angular2/src/facade/async'; -import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; +import {StringMap, StringMapWrapper, ListWrapper, List} from 'angular2/src/facade/collection'; import {Validators} from './validators'; export const VALID = "VALID"; @@ -23,9 +23,12 @@ export class AbstractControl { _status:string; _errors; _pristine:boolean; - _parent:ControlGroup; + _parent:any; /* ControlGroup | ControlArray */ validator:Function; + valueChanges:Observable; + _valueChangesController; + constructor(validator:Function) { this.validator = validator; this._pristine = true; @@ -67,9 +70,6 @@ export class AbstractControl { } export class Control extends AbstractControl { - valueChanges:Observable; - _valueChangesController; - constructor(value:any, validator:Function = Validators.nullValidator) { super(validator); this._setValueErrorsStatus(value); @@ -98,9 +98,6 @@ export class ControlGroup extends AbstractControl { controls; optionals; - valueChanges:Observable; - _valueChangesController; - constructor(controls, optionals = null, validator:Function = Validators.group) { super(validator); this.controls = controls; @@ -170,4 +167,65 @@ export class ControlGroup extends AbstractControl { var isOptional = StringMapWrapper.contains(this.optionals, controlName); return !isOptional || StringMapWrapper.get(this.optionals, controlName); } +} + +export class ControlArray extends AbstractControl { + controls:List; + + constructor(controls:List, validator:Function = Validators.array) { + super(validator); + this.controls = controls; + + this._valueChangesController = ObservableWrapper.createController(); + this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController); + + this._setParentForControls(); + this._setValueErrorsStatus(); + } + + at(index:number) { + return this.controls[index]; + } + + push(control) { + ListWrapper.push(this.controls, control); + control.setParent(this); + this._updateValue(); + } + + insert(index:number, control) { + ListWrapper.insert(this.controls, index, control); + control.setParent(this); + this._updateValue(); + } + + removeAt(index:number) { + ListWrapper.removeAt(this.controls, index); + this._updateValue(); + } + + get length() { + return this.controls.length; + } + + _updateValue() { + this._setValueErrorsStatus(); + this._pristine = false; + + ObservableWrapper.callNext(this._valueChangesController, this._value); + + this._updateParent(); + } + + _setParentForControls() { + ListWrapper.forEach(this.controls, (control) => { + control.setParent(this); + }); + } + + _setValueErrorsStatus() { + this._value = ListWrapper.map(this.controls, (c) => c.value); + this._errors = this.validator(this); + this._status = isPresent(this._errors) ? INVALID : VALID; + } } \ No newline at end of file diff --git a/modules/angular2/src/forms/validators.js b/modules/angular2/src/forms/validators.js index dbce2a560c..0143e4c0ea 100644 --- a/modules/angular2/src/forms/validators.js +++ b/modules/angular2/src/forms/validators.js @@ -26,14 +26,28 @@ export class Validators { var res = {}; StringMapWrapper.forEach(c.controls, (control, name) => { if (c.contains(name) && isPresent(control.errors)) { - StringMapWrapper.forEach(control.errors, (value, error) => { - if (!StringMapWrapper.contains(res, error)) { - res[error] = []; - } - ListWrapper.push(res[error], control); - }); + Validators._mergeErrors(control, res); } }); return StringMapWrapper.isEmpty(res) ? null : res; } + + static array(c:modelModule.ControlArray) { + var res = {}; + ListWrapper.forEach(c.controls, (control) => { + if (isPresent(control.errors)) { + Validators._mergeErrors(control, res); + } + }); + return StringMapWrapper.isEmpty(res) ? null : res; + } + + static _mergeErrors(control, res) { + StringMapWrapper.forEach(control.errors, (value, error) => { + if (!StringMapWrapper.contains(res, error)) { + res[error] = []; + } + ListWrapper.push(res[error], control); + }); + } } diff --git a/modules/angular2/test/forms/form_builder_spec.js b/modules/angular2/test/forms/form_builder_spec.js index 083f9c3250..538dc9eefb 100644 --- a/modules/angular2/test/forms/form_builder_spec.js +++ b/modules/angular2/test/forms/form_builder_spec.js @@ -60,5 +60,18 @@ export function main() { expect(g.controls["login"].validator).toBe(Validators.nullValidator); expect(g.validator).toBe(Validators.group); }); + + it("should create control arrays", () => { + var c = b.control("three"); + var a = b.array([ + "one", + ["two", Validators.required], + c, + b.array(['four']) + ]); + + expect(a.value).toEqual(['one', 'two', 'three', ['four']]); + }); }); -} \ No newline at end of file +} + diff --git a/modules/angular2/test/forms/model_spec.js b/modules/angular2/test/forms/model_spec.js index f1c267df57..2f56fe1c60 100644 --- a/modules/angular2/test/forms/model_spec.js +++ b/modules/angular2/test/forms/model_spec.js @@ -1,6 +1,6 @@ import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el, AsyncTestCompleter, inject} from 'angular2/test_lib'; -import {ControlGroup, Control, OptionalControl, Validators} from 'angular2/forms'; +import {ControlGroup, Control, ControlArray, Validators} from 'angular2/forms';; import {ObservableWrapper} from 'angular2/src/facade/async'; import {ListWrapper} from 'angular2/src/facade/collection'; @@ -50,6 +50,32 @@ export function main() { expect(c.dirty).toEqual(true); }); }); + + describe("valueChanges", () => { + var c; + + beforeEach(() => { + c = new Control("old"); + }); + + it("should fire an event after the value has been updated", inject([AsyncTestCompleter], (async) => { + ObservableWrapper.subscribe(c.valueChanges, (value) => { + expect(c.value).toEqual('new'); + expect(value).toEqual('new'); + async.done(); + }); + c.updateValue("new"); + })); + + it("should return a cold observable", inject([AsyncTestCompleter], (async) => { + c.updateValue("will be ignored"); + ObservableWrapper.subscribe(c.valueChanges, (value) => { + expect(value).toEqual('new'); + async.done(); + }); + c.updateValue("new"); + })); + }); }); describe("ControlGroup", () => { @@ -190,36 +216,8 @@ export function main() { expect(group.valid).toEqual(false); }); - }); - describe("valueChanges", () => { - describe("Control", () => { - var c; - - beforeEach(() => { - c = new Control("old"); - }); - - it("should fire an event after the value has been updated", inject([AsyncTestCompleter], (async) => { - ObservableWrapper.subscribe(c.valueChanges, (value) => { - expect(c.value).toEqual('new'); - expect(value).toEqual('new'); - async.done(); - }); - c.updateValue("new"); - })); - - it("should return a cold observable", inject([AsyncTestCompleter], (async) => { - c.updateValue("will be ignored"); - ObservableWrapper.subscribe(c.valueChanges, (value) => { - expect(value).toEqual('new'); - async.done(); - }); - c.updateValue("new"); - })); - }); - - describe("ControlGroup", () => { + describe("valueChanges", () => { var g, c1, c2; beforeEach(() => { @@ -300,6 +298,162 @@ export function main() { })); }); }); + + describe("ControlArray", () => { + describe("adding/removing", () => { + var a; + var c1, c2, c3; + + beforeEach(() => { + a = new ControlArray([]); + c1 = new Control(1); + c2 = new Control(2); + c3 = new Control(3); + }); + + it("should support pushing", () => { + a.push(c1); + expect(a.length).toEqual(1); + expect(a.controls).toEqual([c1]); + }); + + it("should support removing", () => { + a.push(c1); + a.push(c2); + a.push(c3); + + a.removeAt(1); + + expect(a.controls).toEqual([c1, c3]); + }); + + it("should support inserting", () => { + a.push(c1); + a.push(c3); + + a.insert(1, c2); + + expect(a.controls).toEqual([c1, c2, c3]); + }); + }); + + describe("value", () => { + it("should be the reduced value of the child controls", () => { + var a = new ControlArray([new Control(1), new Control(2)]); + expect(a.value).toEqual([1, 2]); + }); + + it("should be an empty array when there are no child controls", () => { + var a = new ControlArray([]); + expect(a.value).toEqual([]); + }); + }); + + describe("validator", () => { + it("should run the validator with the initial value (valid)", () => { + var a = new ControlArray([ + new Control(1, Validators.required), + new Control(2, Validators.required) + ]); + + expect(a.valid).toBe(true); + expect(a.errors).toBe(null); + }); + + it("should run the validator with the initial value (invalid)", () => { + var a = new ControlArray([ + new Control(1, Validators.required), + new Control(null, Validators.required), + new Control(2, Validators.required) + ]); + + expect(a.valid).toBe(false); + expect(a.errors).toEqual({"required": [a.controls[1]]}); + }); + + it("should run the validator when the value changes", () => { + var a = new ControlArray([]); + var c = new Control(null, Validators.required); + a.push(c); + expect(a.valid).toBe(false); + + c.updateValue("some value"); + + expect(a.valid).toBe(true); + expect(a.errors).toBe(null); + }); + }); + + describe("pristine", () => { + it("should be true after creating a control", () => { + var a = new ControlArray([new Control(1)]); + expect(a.pristine).toBe(true); + }); + + it("should be false after changing the value of the control", () => { + var c = new Control(1); + var a = new ControlArray([c]); + + c.updateValue('new value'); + + expect(a.pristine).toEqual(false); + }); + }); + + describe("valueChanges", () => { + var a, c1, c2; + + beforeEach(() => { + c1 = new Control("old1"); + c2 = new Control("old2") + a = new ControlArray([c1, c2]); + }); + + it("should fire an event after the value has been updated", inject([AsyncTestCompleter], (async) => { + ObservableWrapper.subscribe(a.valueChanges, (value) => { + expect(a.value).toEqual(['new1', 'old2']); + expect(value).toEqual(['new1', 'old2']); + async.done(); + }); + c1.updateValue("new1"); + })); + + it("should fire an event after the control's observable fired an event", inject([AsyncTestCompleter], (async) => { + var controlCallbackIsCalled = false; + + ObservableWrapper.subscribe(c1.valueChanges, (value) => { + controlCallbackIsCalled = true; + }); + + ObservableWrapper.subscribe(a.valueChanges, (value) => { + expect(controlCallbackIsCalled).toBe(true); + async.done(); + }); + + c1.updateValue("new1"); + })); + + it("should fire an event when a control is removed", inject([AsyncTestCompleter], (async) => { + ObservableWrapper.subscribe(a.valueChanges, (value) => { + expect(value).toEqual(['old1']); + async.done(); + }); + + a.removeAt(1); + })); + + it("should fire an event when a control is added", inject([AsyncTestCompleter], (async) => { + a.removeAt(1); + + ObservableWrapper.subscribe(a.valueChanges, (value) => { + expect(value).toEqual(['old1', 'old2']); + async.done(); + }); + + a.push(c2); + })); + }); + }); }); }); } \ No newline at end of file