backbone-associations-rails 0.3.0 → 0.3.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.
@@ -1,12 +1,11 @@
|
|
1
1
|
//
|
2
|
-
//
|
2
|
+
// Backbone-associations.js 0.3.1
|
3
3
|
//
|
4
|
-
//
|
5
|
-
//
|
6
|
-
//
|
7
|
-
//
|
8
|
-
//
|
9
|
-
// A complete [Test & Benchmark Suite](../test/test-suite.html) is included for your perusal.
|
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.
|
10
9
|
|
11
10
|
// Initial Setup
|
12
11
|
// --------------
|
@@ -15,7 +14,8 @@
|
|
15
14
|
|
16
15
|
// The top-level namespace. All public Backbone classes and modules will be attached to this.
|
17
16
|
// Exported for the browser and CommonJS.
|
18
|
-
var _, Backbone, BackboneModel,
|
17
|
+
var _, Backbone, BackboneModel, BackboneCollection, ModelProto,
|
18
|
+
defaultEvents, AssociatedModel;
|
19
19
|
if (typeof require !== 'undefined') {
|
20
20
|
_ = require('underscore');
|
21
21
|
Backbone = require('backbone');
|
@@ -25,9 +25,13 @@
|
|
25
25
|
Backbone = window.Backbone;
|
26
26
|
}
|
27
27
|
// Create local reference `Model` prototype.
|
28
|
-
BackboneModel = Backbone.Model
|
28
|
+
BackboneModel = Backbone.Model;
|
29
|
+
BackboneCollection = Backbone.Collection;
|
30
|
+
ModelProto = BackboneModel.prototype;
|
31
|
+
|
29
32
|
// Built-in Backbone `events`.
|
30
|
-
defaultEvents = ["change", "add", "remove", "reset", "destroy",
|
33
|
+
defaultEvents = ["change", "add", "remove", "reset", "destroy",
|
34
|
+
"sync", "error", "sort", "request"];
|
31
35
|
|
32
36
|
// Backbone.AssociatedModel
|
33
37
|
// --------------
|
@@ -36,17 +40,23 @@
|
|
36
40
|
Backbone.Many = "Many";
|
37
41
|
Backbone.One = "One";
|
38
42
|
// Define `AssociatedModel` (Extends Backbone.Model).
|
39
|
-
AssociatedModel = Backbone.AssociatedModel =
|
43
|
+
AssociatedModel = Backbone.AssociatedModel = BackboneModel.extend({
|
40
44
|
// Define relations with Associated Model.
|
41
|
-
relations:undefined,
|
45
|
+
relations: undefined,
|
42
46
|
// Define `Model` property which can keep track of already fired `events`,
|
43
47
|
// and prevent redundant event to be triggered in case of circular model graph.
|
44
|
-
_proxyCalls:undefined,
|
48
|
+
_proxyCalls: undefined,
|
49
|
+
|
50
|
+
// Get the value of an attribute.
|
51
|
+
get: function(attr){
|
52
|
+
return this.getAttr.apply(this, arguments);
|
53
|
+
},
|
54
|
+
|
45
55
|
// Set a hash of model attributes on the object,
|
46
56
|
// fire Backbone `event` with options.
|
47
57
|
// It maintains relations between models during the set operation.
|
48
58
|
// It also bubbles up child events to the parent.
|
49
|
-
set:function (key, value, options) {
|
59
|
+
set: function (key, value, options) {
|
50
60
|
var attributes, processedRelations, tbp, attr;
|
51
61
|
// Duplicate backbone's behavior to allow separate key/value parameters,
|
52
62
|
// instead of a single 'attributes' object.
|
@@ -68,7 +78,7 @@
|
|
68
78
|
_.each(this.relations, function (relation) {
|
69
79
|
var relationKey = relation.key, relatedModel = relation.relatedModel,
|
70
80
|
collectionType = relation.collectionType,
|
71
|
-
val, relationOptions, data;
|
81
|
+
val, relationOptions, data, relationValue;
|
72
82
|
if (attributes[relationKey]) {
|
73
83
|
//Get value of attribute with relation key in `val`.
|
74
84
|
val = _.result(attributes, relationKey);
|
@@ -81,91 +91,42 @@
|
|
81
91
|
// create `Backbone.Collection` with passed data and perform Backbone `set`.
|
82
92
|
if (relation.type === Backbone.Many) {
|
83
93
|
// `collectionType` of defined `relation` should be instance of `Backbone.Collection`.
|
84
|
-
if (collectionType && !collectionType.prototype instanceof
|
94
|
+
if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
|
85
95
|
throw new Error('collectionType must inherit from Backbone.Collection');
|
86
96
|
}
|
87
97
|
|
88
|
-
|
89
|
-
|
98
|
+
// If `attributes` has no property present,
|
99
|
+
// create `Collection` having `relation.collectionType` as type and
|
100
|
+
// `relation.Model` as model reference and perform Backbone `set`.
|
101
|
+
if (val instanceof BackboneCollection) {
|
102
|
+
ModelProto.set.call(this, relationKey, val, relationOptions);
|
90
103
|
} else if (!this.attributes[relationKey]) {
|
91
|
-
// If `attributes` has no property present,
|
92
|
-
// create `Collection` having `relation.collectionType` as type and
|
93
|
-
// `relation.Model` as model reference, and perform Backbone `set`.
|
94
104
|
data = collectionType ? new collectionType() : this._createCollection(relatedModel);
|
95
105
|
data.add(val, relationOptions);
|
96
|
-
|
106
|
+
ModelProto.set.call(this, relationKey, data, relationOptions);
|
97
107
|
} else {
|
98
108
|
this.attributes[relationKey].reset(val, relationOptions);
|
99
109
|
}
|
100
|
-
|
101
110
|
} else if (relation.type === Backbone.One && relatedModel) {
|
102
111
|
// If passed data is not instance of `Backbone.AssociatedModel`,
|
103
112
|
// create `AssociatedModel` and perform backbone `set`.
|
104
113
|
data = val instanceof AssociatedModel ? val : new relatedModel(val);
|
105
|
-
|
114
|
+
ModelProto.set.call(this, relationKey, data, relationOptions)
|
106
115
|
}
|
107
116
|
|
117
|
+
relationValue = this.attributes[relationKey];
|
118
|
+
|
108
119
|
// Add proxy events to respective parents.
|
109
120
|
// Only add callback if not defined.
|
110
|
-
if (!
|
111
|
-
|
112
|
-
|
113
|
-
opt = args[0].split(":"),
|
114
|
-
eventType = opt[0],
|
115
|
-
eventObject = args[1],
|
116
|
-
indexEventObject = -1,
|
117
|
-
_proxyCalls = this.attributes[relationKey]._proxyCalls,
|
118
|
-
eventPath,
|
119
|
-
eventAvailable;
|
120
|
-
|
121
|
-
// Change the event name to a fully qualified path.
|
122
|
-
if (_.contains(defaultEvents, eventType)) {
|
123
|
-
if (opt && _.size(opt) > 1) {
|
124
|
-
eventPath = opt[1];
|
125
|
-
}
|
126
|
-
// Find the specific object in the collection which has changed.
|
127
|
-
if (this.attributes[relationKey] instanceof Backbone.Collection && "change" === eventType) {
|
128
|
-
indexEventObject = _.indexOf(this.attributes[relationKey].models, eventObject);
|
129
|
-
}
|
130
|
-
// Manipulate `eventPath`.
|
131
|
-
eventPath = relationKey + (indexEventObject !== -1 ?
|
132
|
-
"[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");
|
133
|
-
args[0] = eventType + ":" + eventPath;
|
134
|
-
|
135
|
-
if (_proxyCalls) {
|
136
|
-
// If event has been already triggered as result of same source `eventPath`,
|
137
|
-
// no need to re-trigger event to prevent cycle.
|
138
|
-
eventAvailable = _.find(_proxyCalls, function (value, eventKey) {
|
139
|
-
// `event` ends with eventKey.
|
140
|
-
var d = eventPath.length - eventKey.length;
|
141
|
-
return eventPath.indexOf(eventKey, d) !== -1;
|
142
|
-
});
|
143
|
-
if (eventAvailable) {
|
144
|
-
return this;
|
145
|
-
}
|
146
|
-
} else {
|
147
|
-
_proxyCalls = this.attributes[relationKey]._proxyCalls = {};
|
148
|
-
}
|
149
|
-
// Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
|
150
|
-
_proxyCalls[eventPath] = true;
|
151
|
-
}
|
152
|
-
|
153
|
-
// Bubble up event to parent `model` with new changed arguments.
|
154
|
-
this.trigger.apply(this, args);
|
155
|
-
|
156
|
-
// Remove `eventPath` from `_proxyCalls`,
|
157
|
-
// if `eventPath` and `_proxCalls` are available,
|
158
|
-
// which allow event to be triggered on for next operation of `set`.
|
159
|
-
if (eventPath && _proxyCalls) {
|
160
|
-
delete _proxyCalls[eventPath];
|
161
|
-
}
|
162
|
-
return this;
|
121
|
+
if (relationValue && !relationValue._proxyCallback) {
|
122
|
+
relationValue._proxyCallback = function () {
|
123
|
+
return this._bubbleEvent.call(this, relationKey, arguments);
|
163
124
|
};
|
164
|
-
|
125
|
+
relationValue.on("all", relationValue._proxyCallback, this);
|
165
126
|
}
|
166
127
|
|
167
128
|
// Create a local `processedRelations` array to store the relation key which has been processed.
|
168
|
-
// We cannot use `this.relations` because if there is no value defined for `relationKey`,
|
129
|
+
// We cannot use `this.relations` because if there is no value defined for `relationKey`,
|
169
130
|
// it will not get processed by either `BackboneModel` `set` or the `AssociatedModel` `set`.
|
170
131
|
!processedRelations && (processedRelations = []);
|
171
132
|
if (_.indexOf(processedRelations, relationKey) === -1) {
|
@@ -187,15 +148,73 @@
|
|
187
148
|
tbp = attributes;
|
188
149
|
}
|
189
150
|
// Return results for `BackboneModel.set`.
|
190
|
-
return
|
151
|
+
return ModelProto.set.call(this, tbp, options);
|
152
|
+
},
|
153
|
+
// Bubble-up event to `parent` Model
|
154
|
+
_bubbleEvent: function (relationKey, eventArguments) {
|
155
|
+
var args = eventArguments,
|
156
|
+
opt = args[0].split(":"),
|
157
|
+
eventType = opt[0],
|
158
|
+
eventObject = args[1],
|
159
|
+
indexEventObject = -1,
|
160
|
+
relationValue = this.attributes[relationKey],
|
161
|
+
_proxyCalls = relationValue._proxyCalls,
|
162
|
+
eventPath,
|
163
|
+
eventAvailable;
|
164
|
+
// Change the event name to a fully qualified path.
|
165
|
+
if (_.contains(defaultEvents, eventType)) {
|
166
|
+
_.size(opt) > 1 && (eventPath = opt[1]);
|
167
|
+
// Find the specific object in the collection which has changed.
|
168
|
+
if (relationValue instanceof BackboneCollection && "change" === eventType && eventObject) {
|
169
|
+
//indexEventObject = _.indexOf(relationValue.models, eventObject);
|
170
|
+
var pathTokens = getPathArray(eventPath),
|
171
|
+
initialTokens = _.initial(pathTokens),
|
172
|
+
colModel;
|
173
|
+
colModel = relationValue.find(function (model) {
|
174
|
+
var changedModel = model.get(pathTokens);
|
175
|
+
return eventObject === !(changedModel instanceof AssociatedModel
|
176
|
+
|| changedModel instanceof BackboneCollection)
|
177
|
+
? model.get(initialTokens) : changedModel;
|
178
|
+
});
|
179
|
+
colModel && (indexEventObject = relationValue.indexOf(colModel));
|
180
|
+
}
|
181
|
+
// Manipulate `eventPath`.
|
182
|
+
eventPath = relationKey + (indexEventObject !== -1 ?
|
183
|
+
"[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");
|
184
|
+
args[0] = eventType + ":" + eventPath;
|
185
|
+
|
186
|
+
// If event has been already triggered as result of same source `eventPath`,
|
187
|
+
// no need to re-trigger event to prevent cycle.
|
188
|
+
if (_proxyCalls) {
|
189
|
+
eventAvailable = _.find(_proxyCalls, function (value, eventKey) {
|
190
|
+
return eventPath.indexOf(eventKey, eventPath.length - eventKey.length) !== -1;
|
191
|
+
});
|
192
|
+
if (eventAvailable) return this;
|
193
|
+
} else {
|
194
|
+
_proxyCalls = relationValue._proxyCalls = {};
|
195
|
+
}
|
196
|
+
// Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
|
197
|
+
_proxyCalls[eventPath] = true;
|
198
|
+
}
|
199
|
+
|
200
|
+
// Bubble up event to parent `model` with new changed arguments.
|
201
|
+
this.trigger.apply(this, args);
|
202
|
+
|
203
|
+
// Remove `eventPath` from `_proxyCalls`,
|
204
|
+
// if `eventPath` and `_proxCalls` are available,
|
205
|
+
// which allow event to be triggered on for next operation of `set`.
|
206
|
+
if (eventPath && _proxyCalls) {
|
207
|
+
delete _proxyCalls[eventPath];
|
208
|
+
}
|
209
|
+
return this;
|
191
210
|
},
|
192
211
|
// Returns New `collection` of type `relation.relatedModel`.
|
193
|
-
_createCollection:function (type) {
|
212
|
+
_createCollection: function (type) {
|
194
213
|
var collection, relatedModel = type;
|
195
214
|
_.isString(relatedModel) && (relatedModel = eval(relatedModel));
|
196
215
|
// Creates new `Backbone.Collection` and defines model class.
|
197
216
|
if (relatedModel && relatedModel.prototype instanceof AssociatedModel) {
|
198
|
-
collection = new
|
217
|
+
collection = new BackboneCollection();
|
199
218
|
collection.model = relatedModel;
|
200
219
|
} else {
|
201
220
|
throw new Error('type must inherit from Backbone.AssociatedModel');
|
@@ -203,19 +222,19 @@
|
|
203
222
|
return collection;
|
204
223
|
},
|
205
224
|
// Has the model changed. Traverse the object hierarchy to compute dirtyness.
|
206
|
-
hasChanged:function (attr) {
|
225
|
+
hasChanged: function (attr) {
|
207
226
|
var isDirty, relation, attrValue, i, dirtyObjects;
|
208
227
|
// To prevent cycles, check if this node is visited.
|
209
228
|
if (!this.visitedHC) {
|
210
229
|
this.visitedHC = true;
|
211
|
-
isDirty =
|
230
|
+
isDirty = ModelProto.hasChanged.apply(this, arguments);
|
212
231
|
if (!isDirty && this.relations) {
|
213
232
|
// Go down the hierarchy to see if anything has `changed`.
|
214
233
|
for (i = 0; i < this.relations.length; ++i) {
|
215
234
|
relation = this.relations[i];
|
216
235
|
attrValue = this.attributes[relation.key];
|
217
236
|
if (attrValue) {
|
218
|
-
if (attrValue instanceof
|
237
|
+
if (attrValue instanceof BackboneCollection) {
|
219
238
|
dirtyObjects = attrValue.filter(function (m) {
|
220
239
|
return m.hasChanged() === true;
|
221
240
|
});
|
@@ -234,18 +253,18 @@
|
|
234
253
|
return !!isDirty;
|
235
254
|
},
|
236
255
|
// Returns a hash of the changed attributes.
|
237
|
-
changedAttributes:function (diff) {
|
256
|
+
changedAttributes: function (diff) {
|
238
257
|
var delta, relation, attrValue, changedCollection, i;
|
239
258
|
// To prevent cycles, check if this node is visited.
|
240
259
|
if (!this.visited) {
|
241
260
|
this.visited = true;
|
242
|
-
delta =
|
261
|
+
delta = ModelProto.changedAttributes.apply(this, arguments);
|
243
262
|
if (this.relations) {
|
244
263
|
for (i = 0; i < this.relations.length; ++i) {
|
245
264
|
relation = this.relations[i];
|
246
265
|
attrValue = this.attributes[relation.key];
|
247
266
|
if (attrValue) {
|
248
|
-
if (attrValue instanceof
|
267
|
+
if (attrValue instanceof BackboneCollection) {
|
249
268
|
changedCollection = _.filter(attrValue.map(function (m) {
|
250
269
|
return m.changedAttributes();
|
251
270
|
}), function (m) {
|
@@ -265,12 +284,12 @@
|
|
265
284
|
return !delta ? false : delta;
|
266
285
|
},
|
267
286
|
// Returns the hash of the previous attributes of the graph.
|
268
|
-
previousAttributes:function () {
|
287
|
+
previousAttributes: function () {
|
269
288
|
var pa, attrValue, pattrValue, pattrJSON;
|
270
289
|
// To prevent cycles, check if this node is visited.
|
271
290
|
if (!this.visited) {
|
272
291
|
this.visited = true;
|
273
|
-
pa =
|
292
|
+
pa = ModelProto.previousAttributes.apply(this, arguments);
|
274
293
|
if (this.relations) {
|
275
294
|
_.each(this.relations, function (relation) {
|
276
295
|
attrValue = this.attributes[relation.key];
|
@@ -279,7 +298,7 @@
|
|
279
298
|
if (pattrValue && pattrValue == attrValue) {
|
280
299
|
if (attrValue instanceof AssociatedModel) {
|
281
300
|
pa[relation.key] = attrValue.previousAttributes();
|
282
|
-
} else if (attrValue instanceof
|
301
|
+
} else if (attrValue instanceof BackboneCollection) {
|
283
302
|
pa[relation.key] = attrValue.map(function (m) {
|
284
303
|
return m.previousAttributes();
|
285
304
|
});
|
@@ -295,16 +314,17 @@
|
|
295
314
|
return pa;
|
296
315
|
},
|
297
316
|
// Return the previous value of the passed in attribute.
|
298
|
-
previous:function (attr) {
|
317
|
+
previous: function (attr) {
|
299
318
|
return this.previousAttributes()[attr];
|
300
319
|
},
|
320
|
+
|
301
321
|
// The JSON representation of the model.
|
302
|
-
toJSON:function (options) {
|
322
|
+
toJSON: function (options) {
|
303
323
|
var json, aJson;
|
304
324
|
if (!this.visited) {
|
305
325
|
this.visited = true;
|
306
326
|
// Get json representation from `BackboneModel.toJSON`.
|
307
|
-
json =
|
327
|
+
json = ModelProto.toJSON.apply(this, arguments);
|
308
328
|
// If `this.relations` is defined, iterate through each `relation`
|
309
329
|
// and added it's json representation to parents' json representation.
|
310
330
|
if (this.relations) {
|
@@ -320,37 +340,40 @@
|
|
320
340
|
}
|
321
341
|
return json;
|
322
342
|
},
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
mClone && newCollection.add(mClone);
|
343
|
-
});
|
344
|
-
cloneObj.attributes[relation.key] = newCollection;
|
345
|
-
} else if (sourceObj instanceof Backbone.Model) {
|
346
|
-
cloneObj.attributes[relation.key] = sourceObj.clone();
|
347
|
-
}
|
348
|
-
}
|
349
|
-
}, this);
|
350
|
-
}
|
351
|
-
delete this.visited;
|
343
|
+
|
344
|
+
// Create a new model with identical attributes to this one.
|
345
|
+
clone: function () {
|
346
|
+
return new this.constructor(this.toJSON());
|
347
|
+
},
|
348
|
+
|
349
|
+
// Get `reduced` result using passed `path` array or string.
|
350
|
+
getAttr: function (path, iterator) {
|
351
|
+
var result = this,
|
352
|
+
attrs = getPathArray(path),
|
353
|
+
key,
|
354
|
+
i;
|
355
|
+
iterator || (iterator = function (memo, key) {
|
356
|
+
return memo instanceof BackboneCollection && _.isNumber(key) ? memo.at(key) : memo.attributes[key];
|
357
|
+
});
|
358
|
+
for (i = 0; i < attrs.length; i++) {
|
359
|
+
key = attrs[i];
|
360
|
+
if (!result) break;
|
361
|
+
result = iterator.call(this, result, key, attrs);
|
352
362
|
}
|
353
|
-
return
|
363
|
+
return result;
|
354
364
|
}
|
355
365
|
});
|
366
|
+
|
367
|
+
// Get Path `attrs` as Array
|
368
|
+
// Example:
|
369
|
+
// 'employee.works_for.locations[2].name' -> ['employee', 'works_for', 'locations', 2, 'name']
|
370
|
+
var getPathArray = function (path, iterator, context) {
|
371
|
+
if (_.isString(path)) {
|
372
|
+
iterator || (iterator = function (value) {
|
373
|
+
return value.match(/^\d+$/) ? parseInt(value, 10) : value;
|
374
|
+
});
|
375
|
+
return _.map(path.match(/[^\.\[\]]+/g) || [], iterator, context);
|
376
|
+
}
|
377
|
+
return path || [];
|
378
|
+
}
|
356
379
|
})();
|