feat(forms): added an observable of value changes to Control

This commit is contained in:
vsavkin 2015-03-24 13:45:47 -07:00
parent 9b3b3d325f
commit 19c1773133
3 changed files with 156 additions and 44 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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
}));
});
});
});
});
}
}