Skip to content

Commit 7d45c29

Browse files
committed
Closes #23. Closes #84. #77.
1 parent 6534849 commit 7d45c29

File tree

9 files changed

+283
-8
lines changed

9 files changed

+283
-8
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
##### 0.10.0 - 25 June 2014
1+
##### 0.10.0 - 29 June 2014
22

33
###### Breaking API changes
44
- #76 - Queries and filtering. See [TRANSITION.md](https://github.com/jmdobry/angular-data/blob/master/TRANSITION.md).
@@ -7,9 +7,11 @@
77

88
###### Backwards compatible API changes
99
- #17 - Where predicates should be able to handle OR, not just AND
10+
- #23 - Computed Properties
1011
- #78 - Added optional callback to `bindOne` and `bindAll`
1112
- #79 - `ejectAll` should clear matching completed queries
1213
- #83 - Implement `DS.loadRelations(resourceName, instance(Id), relations[, options])`
14+
- #84 - idAttribute of a resource can be a computed property
1315

1416
##### 0.9.1 - 30 May 2014
1517

TRANSITION.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
### 0.9.x. ---> 0.10.0 - 25 June 2014
1+
### 0.9.x. ---> 0.10.0 - 29 June 2014
22

33
#### Breaking API changes
44
##### #76 - Queries and filtering.
55

6+
###### Before
7+
`IllegalArgumentError` has an `errors` field.
8+
9+
###### After
10+
`IllegalArgumentError` no longer has an `errors` field.
11+
612
###### Before
713
```javascript
814
DS.findAll('post', {

dist/angular-data.js

+48-1
Original file line numberDiff line numberDiff line change
@@ -4093,6 +4093,22 @@ function defineResource(definition) {
40934093
this.utils.deepMixIn(def[def.class].prototype, def.methods);
40944094
}
40954095

4096+
if (def.computed) {
4097+
this.utils.forOwn(def.computed, function (fn, field) {
4098+
if (def.methods && field in def.methods) {
4099+
_this.$log.warn(errorPrefix + 'Computed property "' + field + '" conflicts with previously defined prototype method!');
4100+
}
4101+
var match = fn.toString().match(/function.*?\(([\s\S]*?)\)/);
4102+
var deps = match[1].split(',');
4103+
fn.deps = _this.utils.filter(deps, function (dep) {
4104+
return !!dep;
4105+
});
4106+
angular.forEach(fn.deps, function (val, index) {
4107+
fn.deps[index] = val.trim();
4108+
});
4109+
});
4110+
}
4111+
40964112
this.store[def.name] = {
40974113
collection: [],
40984114
completedQueries: {},
@@ -4104,7 +4120,9 @@ function defineResource(definition) {
41044120
observers: {},
41054121
collectionModified: 0
41064122
};
4107-
} catch (err) {
4123+
}
4124+
catch
4125+
(err) {
41084126
delete this.definitions[definition.name];
41094127
delete this.store[definition.name];
41104128
throw err;
@@ -4691,6 +4709,27 @@ function _inject(definition, resource, attrs) {
46914709
resource.modified[innerId] = _this.utils.updateTimestamp(resource.modified[innerId]);
46924710
resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified);
46934711

4712+
if (definition.computed) {
4713+
var item = _this.get(definition.name, innerId);
4714+
_this.utils.forOwn(definition.computed, function (fn, field) {
4715+
var compute = false;
4716+
// check if required fields changed
4717+
angular.forEach(fn.deps, function (dep) {
4718+
if (dep in changed || dep in removed || dep in changed || !(field in item)) {
4719+
compute = true;
4720+
}
4721+
});
4722+
if (compute) {
4723+
var args = [];
4724+
angular.forEach(fn.deps, function (dep) {
4725+
args.push(item[dep]);
4726+
});
4727+
// recompute property
4728+
item[field] = fn.apply(item, args);
4729+
}
4730+
});
4731+
}
4732+
46944733
if (definition.idAttribute in changed) {
46954734
$log.error('Doh! You just changed the primary key of an object! ' +
46964735
'I don\'t know how to handle this yet, so your data for the "' + definition.name +
@@ -4705,6 +4744,14 @@ function _inject(definition, resource, attrs) {
47054744
injected.push(_inject.call(_this, definition, resource, attrs[i]));
47064745
}
47074746
} else {
4747+
// check if "idAttribute" is a computed property
4748+
if (definition.computed && definition.computed[definition.idAttribute]) {
4749+
var args = [];
4750+
angular.forEach(definition.computed[definition.idAttribute].deps, function (dep) {
4751+
args.push(attrs[dep]);
4752+
});
4753+
attrs[definition.idAttribute] = definition.computed[definition.idAttribute].apply(attrs, args);
4754+
}
47084755
if (!(definition.idAttribute in attrs)) {
47094756
throw new _this.errors.R(errorPrefix + 'attrs: Must contain the property specified by `idAttribute`!');
47104757
} else {

dist/angular-data.min.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

guide/angular-data/resource/resource.md

+51
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,54 @@ You can configure your server to also return the `comment` and `organization` re
394394
```
395395
396396
If you've told angular-data about the relations, then the comments and organization will be injected into the data store in addition to the user.
397+
398+
@doc overview
399+
@id computed
400+
@name Computed Properties
401+
@description
402+
403+
Angular-data supports computed properties. When you define a computed property you also define the fields that it depends on.
404+
The computed property will only be updated when one of those fields changes.
405+
406+
## Example
407+
```js
408+
DS.defineResource('user', {
409+
computed: {
410+
// each function's argument list defines the fields
411+
// that the computed property depends on
412+
fullName: function (first, last) {
413+
return first + ' ' + last;
414+
}
415+
}
416+
});
417+
418+
var user = DS.inject('user', {
419+
id: 1,
420+
first: 'John',
421+
last: 'Anderson'
422+
});
423+
424+
user.fullName; // "John Anderson"
425+
426+
user.first = 'Fred';
427+
428+
// angular-data relies on dirty-checking, so the
429+
// computed property hasn't been updated yet
430+
user.fullName; // "John Anderson"
431+
432+
DS.digest();
433+
434+
user.fullName; // "Fred Anderson"
435+
436+
user.first = 'George';
437+
438+
$timeout(function () {
439+
user.fullName; // "George Anderson"
440+
});
441+
442+
user.first = 'William';
443+
444+
$scope.$apply(function () {
445+
user.fullName; // "William Anderson"
446+
});
447+
```

karma.start.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Setup global test variables
2-
var $rootScope, $q, $log, DSHttpAdapterProvider, DSProvider, DSLocalStorageAdapter, DS, DSUtils, DSHttpAdapter, app, $httpBackend, p1, p2, p3, p4, p5;
2+
var $rootScope, $q, $log, $timeout, DSHttpAdapterProvider, DSProvider, DSLocalStorageAdapter, DS, DSUtils, DSHttpAdapter, app, $httpBackend, p1, p2, p3, p4, p5;
33

44
var user1, organization2, comment3, profile4;
55
var comment11, comment12, comment13, organization14, profile15, user10, user16, user17, user18, organization15, user20, comment19, user22, profile21;
@@ -116,13 +116,14 @@ beforeEach(function () {
116116
});
117117

118118
function startInjector() {
119-
inject(function (_$rootScope_, _$q_, _$httpBackend_, _DS_, _$log_, _DSUtils_, _DSHttpAdapter_, _DSLocalStorageAdapter_) {
119+
inject(function (_$rootScope_, _$q_, _$timeout_, _$httpBackend_, _DS_, _$log_, _DSUtils_, _DSHttpAdapter_, _DSLocalStorageAdapter_) {
120120
// Setup global mocks
121121

122122
localStorage.clear();
123123
$q = _$q_;
124124
$rootScope = _$rootScope_;
125125
DS = _DS_;
126+
$timeout = _$timeout_;
126127
DSUtils = _DSUtils_;
127128
DSHttpAdapter = _DSHttpAdapter_;
128129
DSLocalStorageAdapter = _DSLocalStorageAdapter_;

src/datastore/sync_methods/defineResource.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,22 @@ function defineResource(definition) {
120120
this.utils.deepMixIn(def[def.class].prototype, def.methods);
121121
}
122122

123+
if (def.computed) {
124+
this.utils.forOwn(def.computed, function (fn, field) {
125+
if (def.methods && field in def.methods) {
126+
_this.$log.warn(errorPrefix + 'Computed property "' + field + '" conflicts with previously defined prototype method!');
127+
}
128+
var match = fn.toString().match(/function.*?\(([\s\S]*?)\)/);
129+
var deps = match[1].split(',');
130+
fn.deps = _this.utils.filter(deps, function (dep) {
131+
return !!dep;
132+
});
133+
angular.forEach(fn.deps, function (val, index) {
134+
fn.deps[index] = val.trim();
135+
});
136+
});
137+
}
138+
123139
this.store[def.name] = {
124140
collection: [],
125141
completedQueries: {},
@@ -131,7 +147,9 @@ function defineResource(definition) {
131147
observers: {},
132148
collectionModified: 0
133149
};
134-
} catch (err) {
150+
}
151+
catch
152+
(err) {
135153
delete this.definitions[definition.name];
136154
delete this.store[definition.name];
137155
throw err;

src/datastore/sync_methods/inject.js

+29
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,27 @@ function _inject(definition, resource, attrs) {
1111
resource.modified[innerId] = _this.utils.updateTimestamp(resource.modified[innerId]);
1212
resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified);
1313

14+
if (definition.computed) {
15+
var item = _this.get(definition.name, innerId);
16+
_this.utils.forOwn(definition.computed, function (fn, field) {
17+
var compute = false;
18+
// check if required fields changed
19+
angular.forEach(fn.deps, function (dep) {
20+
if (dep in changed || dep in removed || dep in changed || !(field in item)) {
21+
compute = true;
22+
}
23+
});
24+
if (compute) {
25+
var args = [];
26+
angular.forEach(fn.deps, function (dep) {
27+
args.push(item[dep]);
28+
});
29+
// recompute property
30+
item[field] = fn.apply(item, args);
31+
}
32+
});
33+
}
34+
1435
if (definition.idAttribute in changed) {
1536
$log.error('Doh! You just changed the primary key of an object! ' +
1637
'I don\'t know how to handle this yet, so your data for the "' + definition.name +
@@ -25,6 +46,14 @@ function _inject(definition, resource, attrs) {
2546
injected.push(_inject.call(_this, definition, resource, attrs[i]));
2647
}
2748
} else {
49+
// check if "idAttribute" is a computed property
50+
if (definition.computed && definition.computed[definition.idAttribute]) {
51+
var args = [];
52+
angular.forEach(definition.computed[definition.idAttribute].deps, function (dep) {
53+
args.push(attrs[dep]);
54+
});
55+
attrs[definition.idAttribute] = definition.computed[definition.idAttribute].apply(attrs, args);
56+
}
2857
if (!(definition.idAttribute in attrs)) {
2958
throw new _this.errors.R(errorPrefix + 'attrs: Must contain the property specified by `idAttribute`!');
3059
} else {

test/integration/datastore/sync_methods/defineResource.test.js

+121
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,125 @@ describe('DS.defineResource(definition)', function () {
115115
assert.equal(lifecycle.beforeInject.callCount, 1, 'beforeInject should have been called');
116116
assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called');
117117
});
118+
it('should allow definition of computed properties', function () {
119+
var callCount = 0;
120+
121+
DS.defineResource({
122+
name: 'person',
123+
computed: {
124+
fullName: function (first, last) {
125+
callCount++;
126+
return first + ' ' + last;
127+
}
128+
}
129+
});
130+
131+
DS.inject('person', {
132+
first: 'John',
133+
last: 'Anderson',
134+
135+
id: 1
136+
});
137+
138+
var person = DS.get('person', 1);
139+
140+
assert.deepEqual(JSON.stringify(person), JSON.stringify({
141+
first: 'John',
142+
last: 'Anderson',
143+
144+
id: 1,
145+
fullName: 'John Anderson'
146+
}));
147+
assert.equal(person.fullName, 'John Anderson');
148+
assert.equal(lifecycle.beforeInject.callCount, 1, 'beforeInject should have been called');
149+
assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called');
150+
151+
person.first = 'Johnny';
152+
153+
// digest loop hasn't happened yet
154+
assert.equal(DS.get('person', 1).first, 'Johnny');
155+
assert.equal(DS.get('person', 1).fullName, 'John Anderson');
156+
157+
DS.digest();
158+
159+
assert.deepEqual(person, {
160+
first: 'Johnny',
161+
last: 'Anderson',
162+
163+
id: 1,
164+
fullName: 'Johnny Anderson'
165+
});
166+
assert.equal(person.fullName, 'Johnny Anderson');
167+
168+
// should work with $timeout
169+
$timeout(function () {
170+
person.first = 'Jack';
171+
172+
DS.digest();
173+
174+
assert.deepEqual(person, {
175+
first: 'Jack',
176+
last: 'Anderson',
177+
178+
id: 1,
179+
fullName: 'Jack Anderson'
180+
});
181+
assert.equal(person.fullName, 'Jack Anderson');
182+
});
183+
184+
$timeout.flush();
185+
186+
// computed property function should not be called
187+
// when a property changes that isn't a dependency
188+
// of the computed property
189+
person.email = '[email protected]';
190+
191+
DS.digest();
192+
193+
assert.equal(callCount, 3, 'fullName() should have been called 3 times');
194+
});
195+
it('should work if idAttribute is a computed property computed property', function () {
196+
DS.defineResource({
197+
name: 'person',
198+
computed: {
199+
id: function (first, last) {
200+
return first + '_' + last;
201+
}
202+
}
203+
});
204+
205+
DS.inject('person', {
206+
first: 'John',
207+
last: 'Anderson',
208+
209+
});
210+
211+
var person = DS.get('person', 'John_Anderson');
212+
213+
assert.deepEqual(JSON.stringify(person), JSON.stringify({
214+
first: 'John',
215+
last: 'Anderson',
216+
217+
id: 'John_Anderson'
218+
}));
219+
assert.equal(person.id, 'John_Anderson');
220+
assert.equal(lifecycle.beforeInject.callCount, 1, 'beforeInject should have been called');
221+
assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called');
222+
223+
person.first = 'Johnny';
224+
225+
// digest loop hasn't happened yet
226+
assert.equal(DS.get('person', 'John_Anderson').first, 'Johnny');
227+
assert.equal(DS.get('person', 'John_Anderson').id, 'John_Anderson');
228+
229+
DS.digest();
230+
231+
assert.deepEqual(person, {
232+
first: 'Johnny',
233+
last: 'Anderson',
234+
235+
id: 'Johnny_Anderson'
236+
});
237+
assert.equal(person.id, 'Johnny_Anderson');
238+
});
118239
});

0 commit comments

Comments
 (0)