backbone-associations-rails 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md
CHANGED
@@ -4,7 +4,7 @@ This gem vendors the [Backbone-associations](https://github.com/dhruvaray/backbo
|
|
4
4
|
|
5
5
|
## Dependencies
|
6
6
|
|
7
|
-
```backbone-associations``` requires Backbone.js >= 0.9.
|
7
|
+
```backbone-associations``` requires Backbone.js >= 0.9.10 and Underscore.js >= 1.4.3
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -1,34 +1,39 @@
|
|
1
1
|
//
|
2
|
-
//
|
2
|
+
// Backbone-associations.js 0.4.1
|
3
|
+
//
|
4
|
+
// (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd.
|
5
|
+
// Backbone-associations may be freely distributed under the MIT license.
|
6
|
+
// For all details and documentation:
|
7
|
+
// https://github.com/dhruvaray/backbone-associations/
|
3
8
|
//
|
4
|
-
// (c) 2013 Dhruva Ray, Jaynti Kanani
|
5
|
-
// Backbone-associations may be freely distributed under the MIT license;
|
6
|
-
// see the accompanying LICENSE.txt.
|
7
|
-
// Depends on [Backbone](https://github.com/documentcloud/backbone) and [Underscore](https://github.com/documentcloud/underscore/) as well.
|
8
|
-
// A complete [Test & Benchmark Suite](../test/test-suite.html) is included for your perusal.
|
9
9
|
|
10
10
|
// Initial Setup
|
11
11
|
// --------------
|
12
12
|
(function () {
|
13
13
|
"use strict";
|
14
14
|
|
15
|
+
// Save a reference to the global object (`window` in the browser, `exports`
|
16
|
+
// on the server).
|
17
|
+
var root = this;
|
15
18
|
|
16
19
|
// The top-level namespace. All public Backbone classes and modules will be attached to this.
|
17
20
|
// Exported for the browser and CommonJS.
|
18
21
|
var _, Backbone, BackboneModel, BackboneCollection, ModelProto,
|
19
|
-
defaultEvents, AssociatedModel;
|
22
|
+
defaultEvents, AssociatedModel, pathChecker;
|
23
|
+
|
20
24
|
if (typeof require !== 'undefined') {
|
21
25
|
_ = require('underscore');
|
22
26
|
Backbone = require('backbone');
|
23
27
|
exports = module.exports = Backbone;
|
24
28
|
} else {
|
25
|
-
_ =
|
26
|
-
Backbone =
|
29
|
+
_ = root._;
|
30
|
+
Backbone = root.Backbone;
|
27
31
|
}
|
28
32
|
// Create local reference `Model` prototype.
|
29
33
|
BackboneModel = Backbone.Model;
|
30
34
|
BackboneCollection = Backbone.Collection;
|
31
35
|
ModelProto = BackboneModel.prototype;
|
36
|
+
pathChecker = /[\.\[\]]+/g;
|
32
37
|
|
33
38
|
// Built-in Backbone `events`.
|
34
39
|
defaultEvents = ["change", "add", "remove", "reset", "destroy",
|
@@ -45,12 +50,13 @@
|
|
45
50
|
// Define relations with Associated Model.
|
46
51
|
relations:undefined,
|
47
52
|
// Define `Model` property which can keep track of already fired `events`,
|
48
|
-
// and prevent redundant event to be triggered in case of
|
53
|
+
// and prevent redundant event to be triggered in case of cyclic model graphs.
|
49
54
|
_proxyCalls:undefined,
|
50
55
|
|
51
56
|
// Get the value of an attribute.
|
52
57
|
get:function (attr) {
|
53
|
-
|
58
|
+
var obj = ModelProto.get.call(this, attr);
|
59
|
+
return obj ? obj : this.getAttr.apply(this, arguments);
|
54
60
|
},
|
55
61
|
|
56
62
|
// Set a hash of model attributes on the Backbone Model.
|
@@ -67,14 +73,19 @@
|
|
67
73
|
}
|
68
74
|
if (!attributes) return this;
|
69
75
|
for (attr in attributes) {
|
70
|
-
|
71
|
-
root = this, parentModel = this.get(initials);
|
72
|
-
|
76
|
+
//Create a map for each unique object whose attributes we want to set
|
73
77
|
modelMap || (modelMap = {});
|
74
|
-
if (
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
+
if (attr.match(pathChecker)) {
|
79
|
+
var pathTokens = getPathArray(attr), initials = _.initial(pathTokens), last = pathTokens[pathTokens.length - 1],
|
80
|
+
parentModel = this.get(initials);
|
81
|
+
if (parentModel instanceof AssociatedModel) {
|
82
|
+
obj = modelMap[parentModel.cid] || (modelMap[parentModel.cid] = {'model':parentModel, 'data':{}});
|
83
|
+
obj.data[last] = attributes[attr];
|
84
|
+
}
|
85
|
+
} else {
|
86
|
+
obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
|
87
|
+
obj.data[attr] = attributes[attr];
|
88
|
+
}
|
78
89
|
}
|
79
90
|
if (modelMap) {
|
80
91
|
for (modelId in modelMap) {
|
@@ -82,7 +93,7 @@
|
|
82
93
|
this.setAttr.call(obj.model, obj.data, options) || (result = false);
|
83
94
|
}
|
84
95
|
} else {
|
85
|
-
|
96
|
+
return this.setAttr.call(this, attributes, options);
|
86
97
|
}
|
87
98
|
return result;
|
88
99
|
},
|
@@ -92,7 +103,7 @@
|
|
92
103
|
// It maintains relations between models during the set operation.
|
93
104
|
// It also bubbles up child events to the parent.
|
94
105
|
setAttr:function (attributes, options) {
|
95
|
-
var
|
106
|
+
var attr;
|
96
107
|
// Extract attributes and options.
|
97
108
|
options || (options = {});
|
98
109
|
if (options.unset) for (attr in attributes) attributes[attr] = void 0;
|
@@ -120,106 +131,90 @@
|
|
120
131
|
throw new Error('collectionType must inherit from Backbone.Collection');
|
121
132
|
}
|
122
133
|
|
123
|
-
// If `attributes` has no property present,
|
124
|
-
// create `Collection` having `relation.collectionType` as type and
|
125
|
-
// `relation.Model` as model reference and perform Backbone `set`.
|
126
134
|
if (val instanceof BackboneCollection) {
|
127
|
-
|
128
|
-
|
129
|
-
data = collectionType ? new collectionType() : this._createCollection(relatedModel);
|
130
|
-
data.add(val, relationOptions);
|
131
|
-
ModelProto.set.call(this, relationKey, data, relationOptions);
|
135
|
+
data = val;
|
136
|
+
attributes[relationKey] = data;
|
132
137
|
} else {
|
133
|
-
this.attributes[relationKey]
|
138
|
+
if (!this.attributes[relationKey]) {
|
139
|
+
data = collectionType ? new collectionType() : this._createCollection(relatedModel);
|
140
|
+
data.add(val, relationOptions);
|
141
|
+
attributes[relationKey] = data;
|
142
|
+
} else {
|
143
|
+
this.attributes[relationKey].reset(val, relationOptions);
|
144
|
+
delete attributes[relationKey];
|
145
|
+
}
|
134
146
|
}
|
147
|
+
|
135
148
|
} else if (relation.type === Backbone.One && relatedModel) {
|
136
|
-
// If passed data is not instance of `Backbone.AssociatedModel`,
|
137
|
-
// create `AssociatedModel` and perform backbone `set`.
|
138
149
|
data = val instanceof AssociatedModel ? val : new relatedModel(val);
|
139
|
-
|
150
|
+
attributes[relationKey] = data;
|
140
151
|
}
|
141
152
|
|
142
|
-
relationValue =
|
153
|
+
relationValue = data;
|
143
154
|
|
144
155
|
// Add proxy events to respective parents.
|
145
156
|
// Only add callback if not defined.
|
146
157
|
if (relationValue && !relationValue._proxyCallback) {
|
147
158
|
relationValue._proxyCallback = function () {
|
148
|
-
return this._bubbleEvent.call(this, relationKey, arguments);
|
159
|
+
return this._bubbleEvent.call(this, relationKey, relationValue, arguments);
|
149
160
|
};
|
150
161
|
relationValue.on("all", relationValue._proxyCallback, this);
|
151
162
|
}
|
152
163
|
|
153
|
-
// Create a local `processedRelations` array to store the relation key which has been processed.
|
154
|
-
// We cannot use `this.relations` because if there is no value defined for `relationKey`,
|
155
|
-
// it will not get processed by either `BackboneModel` `set` or the `AssociatedModel` `set`.
|
156
|
-
!processedRelations && (processedRelations = []);
|
157
|
-
if (_.indexOf(processedRelations, relationKey) === -1) {
|
158
|
-
processedRelations.push(relationKey);
|
159
|
-
}
|
160
164
|
}
|
161
165
|
}, this);
|
162
166
|
}
|
163
|
-
if (processedRelations) {
|
164
|
-
// Find attributes yet to be processed - `tbp`.
|
165
|
-
tbp = {};
|
166
|
-
for (attr in attributes) {
|
167
|
-
if (_.indexOf(processedRelations, attr) === -1) {
|
168
|
-
tbp[attr] = attributes[attr];
|
169
|
-
}
|
170
|
-
}
|
171
|
-
} else {
|
172
|
-
// Set all `attributes` to `tbp`.
|
173
|
-
tbp = attributes;
|
174
|
-
}
|
175
167
|
// Return results for `BackboneModel.set`.
|
176
|
-
return ModelProto.set.call(this,
|
168
|
+
return ModelProto.set.call(this, attributes, options);
|
177
169
|
},
|
178
170
|
// Bubble-up event to `parent` Model
|
179
|
-
_bubbleEvent:function (relationKey, eventArguments) {
|
171
|
+
_bubbleEvent:function (relationKey, relationValue, eventArguments) {
|
180
172
|
var args = eventArguments,
|
181
173
|
opt = args[0].split(":"),
|
182
174
|
eventType = opt[0],
|
183
175
|
eventObject = args[1],
|
184
176
|
indexEventObject = -1,
|
185
|
-
relationValue = this.attributes[relationKey],
|
186
177
|
_proxyCalls = relationValue._proxyCalls,
|
187
178
|
eventPath,
|
188
179
|
eventAvailable;
|
189
180
|
// Change the event name to a fully qualified path.
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
var pathTokens = getPathArray(eventPath),
|
196
|
-
initialTokens = _.initial(pathTokens), colModel;
|
181
|
+
_.size(opt) > 1 && (eventPath = opt[1]);
|
182
|
+
// Find the specific object in the collection which has changed.
|
183
|
+
if (relationValue instanceof BackboneCollection && "change" === eventType && eventObject) {
|
184
|
+
var pathTokens = getPathArray(eventPath),
|
185
|
+
initialTokens = _.initial(pathTokens), colModel;
|
197
186
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
187
|
+
colModel = relationValue.find(function (model) {
|
188
|
+
var changedModel = model.get(pathTokens);
|
189
|
+
return eventObject === (changedModel instanceof AssociatedModel
|
190
|
+
|| changedModel instanceof BackboneCollection)
|
191
|
+
? changedModel : (model.get(initialTokens) || model);
|
192
|
+
});
|
193
|
+
colModel && (indexEventObject = relationValue.indexOf(colModel));
|
194
|
+
}
|
195
|
+
// Manipulate `eventPath`.
|
196
|
+
eventPath = relationKey + (indexEventObject !== -1 ?
|
197
|
+
"[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");
|
198
|
+
args[0] = eventType + ":" + eventPath;
|
210
199
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
200
|
+
// If event has been already triggered as result of same source `eventPath`,
|
201
|
+
// no need to re-trigger event to prevent cycle.
|
202
|
+
if (_proxyCalls) {
|
203
|
+
eventAvailable = _.find(_proxyCalls, function (value, eventKey) {
|
204
|
+
return eventPath.indexOf(eventKey, eventPath.length - eventKey.length) !== -1;
|
205
|
+
});
|
206
|
+
if (eventAvailable) return this;
|
207
|
+
} else {
|
208
|
+
_proxyCalls = relationValue._proxyCalls = {};
|
209
|
+
}
|
210
|
+
// Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
|
211
|
+
_proxyCalls[eventPath] = true;
|
212
|
+
|
213
|
+
|
214
|
+
//Set up previous attributes correctly. Backbone v0.9.10 upwards...
|
215
|
+
if ("change" === eventType) {
|
216
|
+
this._previousAttributes[relationKey] = relationValue._previousAttributes;
|
217
|
+
this.changed[relationKey] = relationValue;
|
223
218
|
}
|
224
219
|
|
225
220
|
// Bubble up event to parent `model` with new changed arguments.
|
@@ -246,103 +241,6 @@
|
|
246
241
|
}
|
247
242
|
return collection;
|
248
243
|
},
|
249
|
-
// Has the model changed. Traverse the object hierarchy to compute dirtyness.
|
250
|
-
hasChanged:function (attr) {
|
251
|
-
var isDirty, relation, attrValue, i, dirtyObjects;
|
252
|
-
// To prevent cycles, check if this node is visited.
|
253
|
-
if (!this.visitedHC) {
|
254
|
-
this.visitedHC = true;
|
255
|
-
isDirty = ModelProto.hasChanged.apply(this, arguments);
|
256
|
-
if (!isDirty && this.relations) {
|
257
|
-
// Go down the hierarchy to see if anything has `changed`.
|
258
|
-
for (i = 0; i < this.relations.length; ++i) {
|
259
|
-
relation = this.relations[i];
|
260
|
-
attrValue = this.attributes[relation.key];
|
261
|
-
if (attrValue) {
|
262
|
-
if (attrValue instanceof BackboneCollection) {
|
263
|
-
dirtyObjects = attrValue.filter(function (m) {
|
264
|
-
return m.hasChanged() === true;
|
265
|
-
});
|
266
|
-
_.size(dirtyObjects) > 0 && (isDirty = true);
|
267
|
-
} else {
|
268
|
-
isDirty = attrValue.hasChanged && attrValue.hasChanged();
|
269
|
-
}
|
270
|
-
if (isDirty) {
|
271
|
-
break;
|
272
|
-
}
|
273
|
-
}
|
274
|
-
}
|
275
|
-
}
|
276
|
-
delete this.visitedHC;
|
277
|
-
}
|
278
|
-
return !!isDirty;
|
279
|
-
},
|
280
|
-
// Returns a hash of the changed attributes.
|
281
|
-
changedAttributes:function (diff) {
|
282
|
-
var delta, relation, attrValue, changedCollection, i;
|
283
|
-
// To prevent cycles, check if this node is visited.
|
284
|
-
if (!this.visited) {
|
285
|
-
this.visited = true;
|
286
|
-
delta = ModelProto.changedAttributes.apply(this, arguments);
|
287
|
-
if (this.relations) {
|
288
|
-
for (i = 0; i < this.relations.length; ++i) {
|
289
|
-
relation = this.relations[i];
|
290
|
-
attrValue = this.attributes[relation.key];
|
291
|
-
if (attrValue) {
|
292
|
-
if (attrValue instanceof BackboneCollection) {
|
293
|
-
changedCollection = _.filter(attrValue.map(function (m) {
|
294
|
-
return m.changedAttributes();
|
295
|
-
}), function (m) {
|
296
|
-
return !!m;
|
297
|
-
});
|
298
|
-
if (_.size(changedCollection) > 0) {
|
299
|
-
delta[relation.key] = changedCollection;
|
300
|
-
}
|
301
|
-
} else if (attrValue instanceof AssociatedModel && attrValue.hasChanged()) {
|
302
|
-
delta[relation.key] = attrValue.toJSON();
|
303
|
-
}
|
304
|
-
}
|
305
|
-
}
|
306
|
-
}
|
307
|
-
delete this.visited;
|
308
|
-
}
|
309
|
-
return !delta ? false : delta;
|
310
|
-
},
|
311
|
-
// Returns the hash of the previous attributes of the graph.
|
312
|
-
previousAttributes:function () {
|
313
|
-
var pa, attrValue, pattrValue, pattrJSON;
|
314
|
-
// To prevent cycles, check if this node is visited.
|
315
|
-
if (!this.visited) {
|
316
|
-
this.visited = true;
|
317
|
-
pa = ModelProto.previousAttributes.apply(this, arguments);
|
318
|
-
if (this.relations) {
|
319
|
-
_.each(this.relations, function (relation) {
|
320
|
-
attrValue = this.attributes[relation.key];
|
321
|
-
pattrValue = pa[relation.key];
|
322
|
-
pattrJSON = pattrValue ? pattrValue.toJSON() : undefined;
|
323
|
-
if (pattrValue && pattrValue == attrValue) {
|
324
|
-
if (attrValue instanceof AssociatedModel) {
|
325
|
-
pa[relation.key] = attrValue.previousAttributes();
|
326
|
-
} else if (attrValue instanceof BackboneCollection) {
|
327
|
-
pa[relation.key] = attrValue.map(function (m) {
|
328
|
-
return m.previousAttributes();
|
329
|
-
});
|
330
|
-
}
|
331
|
-
} else {
|
332
|
-
if (pattrValue)
|
333
|
-
pa[relation.key] = pattrJSON;
|
334
|
-
}
|
335
|
-
}, this);
|
336
|
-
}
|
337
|
-
delete this.visited;
|
338
|
-
}
|
339
|
-
return pa;
|
340
|
-
},
|
341
|
-
// Return the previous value of the passed in attribute.
|
342
|
-
previous:function (attr) {
|
343
|
-
return this.previousAttributes()[attr];
|
344
|
-
},
|
345
|
-
|
346
244
|
// The JSON representation of the model.
|
347
245
|
toJSON:function (options) {
|
348
246
|
var json, aJson;
|
@@ -371,36 +269,30 @@
|
|
371
269
|
return new this.constructor(this.toJSON());
|
372
270
|
},
|
373
271
|
|
374
|
-
//
|
375
|
-
getAttr:function (path
|
272
|
+
//Navigate the path to the leaf object in the path to query for the attribute value
|
273
|
+
getAttr:function (path) {
|
274
|
+
|
376
275
|
var result = this,
|
276
|
+
//Tokenize the path
|
377
277
|
attrs = getPathArray(path),
|
378
278
|
key,
|
379
279
|
i;
|
380
280
|
if (_.size(attrs) < 1) return;
|
381
|
-
iterator || (iterator = function (memo, key) {
|
382
|
-
return memo instanceof BackboneCollection && _.isNumber(key) ? memo.at(key) : memo.attributes[key];
|
383
|
-
});
|
384
281
|
for (i = 0; i < attrs.length; i++) {
|
385
282
|
key = attrs[i];
|
386
283
|
if (!result) break;
|
387
|
-
|
284
|
+
//Navigate the path to get to the result
|
285
|
+
result = result instanceof BackboneCollection && (!isNaN(key)) ? result.at(key) : result.attributes[key];
|
388
286
|
}
|
389
287
|
return result;
|
390
288
|
}
|
391
289
|
});
|
392
290
|
|
393
|
-
var
|
394
|
-
var _pathTokenizer = /[^\.\[\]]+/g;
|
291
|
+
var delimiters = /[^\.\[\]]+/g;
|
395
292
|
|
396
|
-
//
|
397
|
-
var getPathArray = function (path
|
398
|
-
if (
|
399
|
-
|
400
|
-
return value.match(_index) ? parseInt(value, 10) : value;
|
401
|
-
});
|
402
|
-
return _.map(path.match(_pathTokenizer) || [''], iterator, context);
|
403
|
-
}
|
404
|
-
return path || [''];
|
293
|
+
// Tokenize the fully qualified event path
|
294
|
+
var getPathArray = function (path) {
|
295
|
+
if (path === '') return [''];
|
296
|
+
return _.isString(path) ? (path.match(delimiters)) : path || [];
|
405
297
|
}
|
406
|
-
})();
|
298
|
+
}).call(this);
|