diff --git a/modules/angular2/src/forms/model.js b/modules/angular2/src/forms/model.js index ae63afd135..62698ef371 100644 --- a/modules/angular2/src/forms/model.js +++ b/modules/angular2/src/forms/model.js @@ -1,4 +1,5 @@ import {isPresent} from 'angular2/src/facade/lang'; +import {Observable, ObservableWrapper} from 'angular2/src/facade/async'; import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; import {Validators} from './validators'; @@ -21,39 +22,32 @@ export class AbstractControl { _value:any; _status:string; _errors; - _updateNeeded:boolean; _pristine:boolean; _parent:ControlGroup; validator:Function; constructor(validator:Function) { this.validator = validator; - this._updateNeeded = true; this._pristine = true; } 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; } get pristine() { - this._updateIfNeeded(); return this._pristine; } @@ -65,35 +59,38 @@ export class AbstractControl { this._parent = parent; } - _updateIfNeeded() { - } - _updateParent() { if (isPresent(this._parent)){ - this._parent._controlChanged(); + this._parent._updateValue(); } } } export class Control extends AbstractControl { + valueChanges:Observable; + _valueChangesController; + constructor(value:any, validator:Function = Validators.nullValidator) { super(validator); - this._value = value; + this._setValueErrorsStatus(value); + + this._valueChangesController = ObservableWrapper.createController(); + this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController); } updateValue(value:any) { - this._value = value; - this._updateNeeded = true; + this._setValueErrorsStatus(value); this._pristine = false; + + ObservableWrapper.callNext(this._valueChangesController, this._value); + this._updateParent(); } - _updateIfNeeded() { - if (this._updateNeeded) { - this._updateNeeded = false; - this._errors = this.validator(this); - this._status = isPresent(this._errors) ? INVALID : VALID; - } + _setValueErrorsStatus(value) { + this._value = value; + this._errors = this.validator(this); + this._status = isPresent(this._errors) ? INVALID : VALID; } } @@ -101,21 +98,29 @@ export class ControlGroup extends AbstractControl { controls; optionals; + valueChanges:Observable; + _valueChangesController; + constructor(controls, optionals = null, validator:Function = Validators.group) { super(validator); this.controls = controls; this.optionals = isPresent(optionals) ? optionals : {}; + + this._valueChangesController = ObservableWrapper.createController(); + this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController); + this._setParentForControls(); + this._setValueErrorsStatus(); } include(controlName:string) { - this._updateNeeded = true; StringMapWrapper.set(this.optionals, controlName, true); + this._updateValue(); } exclude(controlName:string) { - this._updateNeeded = true; StringMapWrapper.set(this.optionals, controlName, false); + this._updateValue(); } contains(controlName:string) { @@ -129,14 +134,19 @@ export class ControlGroup extends AbstractControl { }); } - _updateIfNeeded() { - if (this._updateNeeded) { - this._updateNeeded = false; - this._value = this._reduceValue(); - this._pristine = this._reducePristine(); - this._errors = this.validator(this); - this._status = isPresent(this._errors) ? INVALID : VALID; - } + _updateValue() { + this._setValueErrorsStatus(); + this._pristine = false; + + ObservableWrapper.callNext(this._valueChangesController, this._value); + + this._updateParent(); + } + + _setValueErrorsStatus() { + this._value = this._reduceValue(); + this._errors = this.validator(this); + this._status = isPresent(this._errors) ? INVALID : VALID; } _reduceValue() { @@ -146,11 +156,6 @@ export class ControlGroup extends AbstractControl { }); } - _reducePristine() { - return this._reduceChildren(true, - (acc, control, name) => acc && control.pristine); - } - _reduceChildren(initValue, fn:Function) { var res = initValue; StringMapWrapper.forEach(this.controls, (control, name) => { @@ -161,11 +166,6 @@ export class ControlGroup extends AbstractControl { return res; } - _controlChanged() { - this._updateNeeded = true; - this._updateParent(); - } - _included(controlName:string):boolean { var isOptional = StringMapWrapper.contains(this.optionals, controlName); return !isOptional || StringMapWrapper.get(this.optionals, controlName); diff --git a/modules/angular2/src/forms/validators.js b/modules/angular2/src/forms/validators.js index fd5e3ccbe0..dbce2a560c 100644 --- a/modules/angular2/src/forms/validators.js +++ b/modules/angular2/src/forms/validators.js @@ -8,7 +8,7 @@ export class Validators { return isBlank(c.value) || c.value == "" ? {"required": true} : null; } - static nullValidator(c:modelModule.Control) { + static nullValidator(c:any) { return null; } diff --git a/modules/angular2/test/forms/model_spec.js b/modules/angular2/test/forms/model_spec.js index 69a732c3b8..5252e45b0d 100644 --- a/modules/angular2/test/forms/model_spec.js +++ b/modules/angular2/test/forms/model_spec.js @@ -1,5 +1,8 @@ -import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el} from 'angular2/test_lib'; -import {ControlGroup, Control, OptionalControl, Validators} from 'angular2/forms'; +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 {ObservableWrapper} from 'angular2/src/facade/async'; +import {ListWrapper} from 'angular2/src/facade/collection'; export function main() { describe("Form Model", () => { @@ -188,6 +191,115 @@ 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", () => { + var g, c1, c2; + + beforeEach(() => { + c1 = new Control("old1"); + c2 = new Control("old2") + g = new ControlGroup({ + "one" : c1, "two" : c2 + }, { + "two" : true + }); + }); + + it("should fire an event after the value has been updated", inject([AsyncTestCompleter], (async) => { + ObservableWrapper.subscribe(g.valueChanges, (value) => { + expect(g.value).toEqual({'one' : 'new1', 'two' : 'old2'}); + expect(value).toEqual({'one' : 'new1', 'two' : '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(g.valueChanges, (value) => { + expect(controlCallbackIsCalled).toBe(true); + async.done(); + }); + + c1.updateValue("new1"); + })); + + it("should fire an event when a control is excluded", inject([AsyncTestCompleter], (async) => { + ObservableWrapper.subscribe(g.valueChanges, (value) => { + expect(value).toEqual({'one' : 'old1'}); + async.done(); + }); + + g.exclude("two"); + })); + + it("should fire an event when a control is included", inject([AsyncTestCompleter], (async) => { + g.exclude("two"); + + ObservableWrapper.subscribe(g.valueChanges, (value) => { + expect(value).toEqual({'one' : 'old1', 'two' : 'old2'}); + async.done(); + }); + + g.include("two"); + })); + + it("should fire an event every time a control is updated", inject([AsyncTestCompleter], (async) => { + var loggedValues = []; + + ObservableWrapper.subscribe(g.valueChanges, (value) => { + ListWrapper.push(loggedValues, value); + + if (loggedValues.length == 2) { + expect(loggedValues).toEqual([ + {"one" : "new1", "two" : "old2"}, + {"one" : "new1", "two" : "new2"} + ]) + async.done(); + } + }); + + c1.updateValue("new1"); + c2.updateValue("new2"); + })); + + xit("should not fire an event when an excluded control is updated", inject([AsyncTestCompleter], (async) => { + // hard to test without hacking zones + })); + }); + }); }); }); -} +} \ No newline at end of file