backbone-associations-rails 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
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);
|