Skip to content

Commit 19c1773

Browse files
committed
feat(forms): added an observable of value changes to Control
1 parent 9b3b3d3 commit 19c1773

File tree

3 files changed

+156
-44
lines changed

3 files changed

+156
-44
lines changed

modules/angular2/src/forms/model.js

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {isPresent} from 'angular2/src/facade/lang';
2+
import {Observable, ObservableWrapper} from 'angular2/src/facade/async';
23
import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
34
import {Validators} from './validators';
45

@@ -21,39 +22,32 @@ export class AbstractControl {
2122
_value:any;
2223
_status:string;
2324
_errors;
24-
_updateNeeded:boolean;
2525
_pristine:boolean;
2626
_parent:ControlGroup;
2727
validator:Function;
2828

2929
constructor(validator:Function) {
3030
this.validator = validator;
31-
this._updateNeeded = true;
3231
this._pristine = true;
3332
}
3433

3534
get value() {
36-
this._updateIfNeeded();
3735
return this._value;
3836
}
3937

4038
get status() {
41-
this._updateIfNeeded();
4239
return this._status;
4340
}
4441

4542
get valid() {
46-
this._updateIfNeeded();
4743
return this._status === VALID;
4844
}
4945

5046
get errors() {
51-
this._updateIfNeeded();
5247
return this._errors;
5348
}
5449

5550
get pristine() {
56-
this._updateIfNeeded();
5751
return this._pristine;
5852
}
5953

@@ -65,57 +59,68 @@ export class AbstractControl {
6559
this._parent = parent;
6660
}
6761

68-
_updateIfNeeded() {
69-
}
70-
7162
_updateParent() {
7263
if (isPresent(this._parent)){
73-
this._parent._controlChanged();
64+
this._parent._updateValue();
7465
}
7566
}
7667
}
7768

7869
export class Control extends AbstractControl {
70+
valueChanges:Observable;
71+
_valueChangesController;
72+
7973
constructor(value:any, validator:Function = Validators.nullValidator) {
8074
super(validator);
81-
this._value = value;
75+
this._setValueErrorsStatus(value);
76+
77+
this._valueChangesController = ObservableWrapper.createController();
78+
this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController);
8279
}
8380

8481
updateValue(value:any) {
85-
this._value = value;
86-
this._updateNeeded = true;
82+
this._setValueErrorsStatus(value);
8783
this._pristine = false;
84+
85+
ObservableWrapper.callNext(this._valueChangesController, this._value);
86+
8887
this._updateParent();
8988
}
9089

91-
_updateIfNeeded() {
92-
if (this._updateNeeded) {
93-
this._updateNeeded = false;
94-
this._errors = this.validator(this);
95-
this._status = isPresent(this._errors) ? INVALID : VALID;
96-
}
90+
_setValueErrorsStatus(value) {
91+
this._value = value;
92+
this._errors = this.validator(this);
93+
this._status = isPresent(this._errors) ? INVALID : VALID;
9794
}
9895
}
9996

10097
export class ControlGroup extends AbstractControl {
10198
controls;
10299
optionals;
103100

101+
valueChanges:Observable;
102+
_valueChangesController;
103+
104104
constructor(controls, optionals = null, validator:Function = Validators.group) {
105105
super(validator);
106106
this.controls = controls;
107107
this.optionals = isPresent(optionals) ? optionals : {};
108+
109+
this._valueChangesController = ObservableWrapper.createController();
110+
this.valueChanges = ObservableWrapper.createObservable(this._valueChangesController);
111+
108112
this._setParentForControls();
113+
this._setValueErrorsStatus();
109114
}
110115

111116
include(controlName:string) {
112-
this._updateNeeded = true;
113117
StringMapWrapper.set(this.optionals, controlName, true);
118+
this._updateValue();
114119
}
115120

116121
exclude(controlName:string) {
117-
this._updateNeeded = true;
118122
StringMapWrapper.set(this.optionals, controlName, false);
123+
this._updateValue();
119124
}
120125

121126
contains(controlName:string) {
@@ -129,14 +134,19 @@ export class ControlGroup extends AbstractControl {
129134
});
130135
}
131136

132-
_updateIfNeeded() {
133-
if (this._updateNeeded) {
134-
this._updateNeeded = false;
135-
this._value = this._reduceValue();
136-
this._pristine = this._reducePristine();
137-
this._errors = this.validator(this);
138-
this._status = isPresent(this._errors) ? INVALID : VALID;
139-
}
137+
_updateValue() {
138+
this._setValueErrorsStatus();
139+
this._pristine = false;
140+
141+
ObservableWrapper.callNext(this._valueChangesController, this._value);
142+
143+
this._updateParent();
144+
}
145+
146+
_setValueErrorsStatus() {
147+
this._value = this._reduceValue();
148+
this._errors = this.validator(this);
149+
this._status = isPresent(this._errors) ? INVALID : VALID;
140150
}
141151

142152
_reduceValue() {
@@ -146,11 +156,6 @@ export class ControlGroup extends AbstractControl {
146156
});
147157
}
148158

149-
_reducePristine() {
150-
return this._reduceChildren(true,
151-
(acc, control, name) => acc && control.pristine);
152-
}
153-
154159
_reduceChildren(initValue, fn:Function) {
155160
var res = initValue;
156161
StringMapWrapper.forEach(this.controls, (control, name) => {
@@ -161,11 +166,6 @@ export class ControlGroup extends AbstractControl {
161166
return res;
162167
}
163168

164-
_controlChanged() {
165-
this._updateNeeded = true;
166-
this._updateParent();
167-
}
168-
169169
_included(controlName:string):boolean {
170170
var isOptional = StringMapWrapper.contains(this.optionals, controlName);
171171
return !isOptional || StringMapWrapper.get(this.optionals, controlName);

modules/angular2/src/forms/validators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class Validators {
88
return isBlank(c.value) || c.value == "" ? {"required": true} : null;
99
}
1010

11-
static nullValidator(c:modelModule.Control) {
11+
static nullValidator(c:any) {
1212
return null;
1313
}
1414

modules/angular2/test/forms/model_spec.js

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el} from 'angular2/test_lib';
2-
import {ControlGroup, Control, OptionalControl, Validators} from 'angular2/forms';
1+
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el,
2+
AsyncTestCompleter, inject} from 'angular2/test_lib';
3+
import {ControlGroup, Control, OptionalControl, Validators} from 'angular2/forms';;
4+
import {ObservableWrapper} from 'angular2/src/facade/async';
5+
import {ListWrapper} from 'angular2/src/facade/collection';
36

47
export function main() {
58
describe("Form Model", () => {
@@ -188,6 +191,115 @@ export function main() {
188191
expect(group.valid).toEqual(false);
189192
});
190193
});
194+
195+
describe("valueChanges", () => {
196+
describe("Control", () => {
197+
var c;
198+
199+
beforeEach(() => {
200+
c = new Control("old");
201+
});
202+
203+
it("should fire an event after the value has been updated", inject([AsyncTestCompleter], (async) => {
204+
ObservableWrapper.subscribe(c.valueChanges, (value) => {
205+
expect(c.value).toEqual('new');
206+
expect(value).toEqual('new');
207+
async.done();
208+
});
209+
c.updateValue("new");
210+
}));
211+
212+
it("should return a cold observable", inject([AsyncTestCompleter], (async) => {
213+
c.updateValue("will be ignored");
214+
ObservableWrapper.subscribe(c.valueChanges, (value) => {
215+
expect(value).toEqual('new');
216+
async.done();
217+
});
218+
c.updateValue("new");
219+
}));
220+
});
221+
222+
describe("ControlGroup", () => {
223+
var g, c1, c2;
224+
225+
beforeEach(() => {
226+
c1 = new Control("old1");
227+
c2 = new Control("old2")
228+
g = new ControlGroup({
229+
"one" : c1, "two" : c2
230+
}, {
231+
"two" : true
232+
});
233+
});
234+
235+
it("should fire an event after the value has been updated", inject([AsyncTestCompleter], (async) => {
236+
ObservableWrapper.subscribe(g.valueChanges, (value) => {
237+
expect(g.value).toEqual({'one' : 'new1', 'two' : 'old2'});
238+
expect(value).toEqual({'one' : 'new1', 'two' : 'old2'});
239+
async.done();
240+
});
241+
c1.updateValue("new1");
242+
}));
243+
244+
it("should fire an event after the control's observable fired an event", inject([AsyncTestCompleter], (async) => {
245+
var controlCallbackIsCalled = false;
246+
247+
ObservableWrapper.subscribe(c1.valueChanges, (value) => {
248+
controlCallbackIsCalled = true;
249+
});
250+
251+
ObservableWrapper.subscribe(g.valueChanges, (value) => {
252+
expect(controlCallbackIsCalled).toBe(true);
253+
async.done();
254+
});
255+
256+
c1.updateValue("new1");
257+
}));
258+
259+
it("should fire an event when a control is excluded", inject([AsyncTestCompleter], (async) => {
260+
ObservableWrapper.subscribe(g.valueChanges, (value) => {
261+
expect(value).toEqual({'one' : 'old1'});
262+
async.done();
263+
});
264+
265+
g.exclude("two");
266+
}));
267+
268+
it("should fire an event when a control is included", inject([AsyncTestCompleter], (async) => {
269+
g.exclude("two");
270+
271+
ObservableWrapper.subscribe(g.valueChanges, (value) => {
272+
expect(value).toEqual({'one' : 'old1', 'two' : 'old2'});
273+
async.done();
274+
});
275+
276+
g.include("two");
277+
}));
278+
279+
it("should fire an event every time a control is updated", inject([AsyncTestCompleter], (async) => {
280+
var loggedValues = [];
281+
282+
ObservableWrapper.subscribe(g.valueChanges, (value) => {
283+
ListWrapper.push(loggedValues, value);
284+
285+
if (loggedValues.length == 2) {
286+
expect(loggedValues).toEqual([
287+
{"one" : "new1", "two" : "old2"},
288+
{"one" : "new1", "two" : "new2"}
289+
])
290+
async.done();
291+
}
292+
});
293+
294+
c1.updateValue("new1");
295+
c2.updateValue("new2");
296+
}));
297+
298+
xit("should not fire an event when an excluded control is updated", inject([AsyncTestCompleter], (async) => {
299+
// hard to test without hacking zones
300+
}));
301+
});
302+
});
191303
});
192304
});
193-
}
305+
}

0 commit comments

Comments
 (0)