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 {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} from 'angular2/src/facade/collection';
import {Validators} from './validators'; import {Validators} from './validators';
@ -21,39 +22,32 @@ export class AbstractControl {
_value:any; _value:any;
_status:string; _status:string;
_errors; _errors;
_updateNeeded:boolean;
_pristine:boolean; _pristine:boolean;
_parent:ControlGroup; _parent:ControlGroup;
validator:Function; validator:Function;
constructor(validator:Function) { constructor(validator:Function) {
this.validator = validator; this.validator = validator;
this._updateNeeded = true;
this._pristine = true; this._pristine = true;
} }
get value() { get value() {
this._updateIfNeeded();
return this._value; return this._value;
} }
get status() { get status() {
this._updateIfNeeded();
return this._status; return this._status;
} }
get valid() { get valid() {
this._updateIfNeeded();
return this._status === VALID; return this._status === VALID;
} }
get errors() { get errors() {
this._updateIfNeeded();
return this._errors; return this._errors;
} }
get pristine() { get pristine() {
this._updateIfNeeded();
return this._pristine; return this._pristine;
} }
@ -65,57 +59,68 @@ export class AbstractControl {
this._parent = parent; this._parent = parent;
} }
_updateIfNeeded() {
}
_updateParent() { _updateParent() {
if (isPresent(this._parent)){ if (isPresent(this._parent)){
this._parent._controlChanged(); this._parent._updateValue();
} }
} }
} }
export class Control extends AbstractControl { export class Control extends AbstractControl {
valueChanges:Observable;
_valueChangesController;
constructor(value:any, validator:Function = Validators.nullValidator) { constructor(value:any, validator:Function = Validators.nullValidator) {
super(validator); super(validator);
this._value = value; this._setValueErrorsStatus(value);
this._valueChangesController = ObservableWrapper.createController();
this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController);
} }
updateValue(value:any) { updateValue(value:any) {
this._value = value; this._setValueErrorsStatus(value);
this._updateNeeded = true;
this._pristine = false; this._pristine = false;
ObservableWrapper.callNext(this._valueChangesController, this._value);
this._updateParent(); this._updateParent();
} }
_updateIfNeeded() { _setValueErrorsStatus(value) {
if (this._updateNeeded) { this._value = value;
this._updateNeeded = false;
this._errors = this.validator(this); this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID; this._status = isPresent(this._errors) ? INVALID : VALID;
} }
}
} }
export class ControlGroup extends AbstractControl { export class ControlGroup extends AbstractControl {
controls; controls;
optionals; optionals;
valueChanges:Observable;
_valueChangesController;
constructor(controls, optionals = null, validator:Function = Validators.group) { constructor(controls, optionals = null, validator:Function = Validators.group) {
super(validator); super(validator);
this.controls = controls; this.controls = controls;
this.optionals = isPresent(optionals) ? optionals : {}; this.optionals = isPresent(optionals) ? optionals : {};
this._valueChangesController = ObservableWrapper.createController();
this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController);
this._setParentForControls(); this._setParentForControls();
this._setValueErrorsStatus();
} }
include(controlName:string) { include(controlName:string) {
this._updateNeeded = true;
StringMapWrapper.set(this.optionals, controlName, true); StringMapWrapper.set(this.optionals, controlName, true);
this._updateValue();
} }
exclude(controlName:string) { exclude(controlName:string) {
this._updateNeeded = true;
StringMapWrapper.set(this.optionals, controlName, false); StringMapWrapper.set(this.optionals, controlName, false);
this._updateValue();
} }
contains(controlName:string) { contains(controlName:string) {
@ -129,15 +134,20 @@ export class ControlGroup extends AbstractControl {
}); });
} }
_updateIfNeeded() { _updateValue() {
if (this._updateNeeded) { this._setValueErrorsStatus();
this._updateNeeded = false; this._pristine = false;
ObservableWrapper.callNext(this._valueChangesController, this._value);
this._updateParent();
}
_setValueErrorsStatus() {
this._value = this._reduceValue(); this._value = this._reduceValue();
this._pristine = this._reducePristine();
this._errors = this.validator(this); this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID; this._status = isPresent(this._errors) ? INVALID : VALID;
} }
}
_reduceValue() { _reduceValue() {
return this._reduceChildren({}, (acc, control, name) => { return this._reduceChildren({}, (acc, control, name) => {
@ -146,11 +156,6 @@ export class ControlGroup extends AbstractControl {
}); });
} }
_reducePristine() {
return this._reduceChildren(true,
(acc, control, name) => acc && control.pristine);
}
_reduceChildren(initValue, fn:Function) { _reduceChildren(initValue, fn:Function) {
var res = initValue; var res = initValue;
StringMapWrapper.forEach(this.controls, (control, name) => { StringMapWrapper.forEach(this.controls, (control, name) => {
@ -161,11 +166,6 @@ export class ControlGroup extends AbstractControl {
return res; return res;
} }
_controlChanged() {
this._updateNeeded = true;
this._updateParent();
}
_included(controlName:string):boolean { _included(controlName:string):boolean {
var isOptional = StringMapWrapper.contains(this.optionals, controlName); var isOptional = StringMapWrapper.contains(this.optionals, controlName);
return !isOptional || StringMapWrapper.get(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; return isBlank(c.value) || c.value == "" ? {"required": true} : null;
} }
static nullValidator(c:modelModule.Control) { static nullValidator(c:any) {
return null; return null;
} }

View File

@ -1,5 +1,8 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el} from 'angular2/test_lib'; import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el,
import {ControlGroup, Control, OptionalControl, Validators} from 'angular2/forms'; 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() { export function main() {
describe("Form Model", () => { describe("Form Model", () => {
@ -188,6 +191,115 @@ export function main() {
expect(group.valid).toEqual(false); 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
}));
});
});
}); });
}); });
} }