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.2 and Underscore.js >= 1.3.3
7
+ ```backbone-associations``` requires Backbone.js >= 0.9.10 and Underscore.js >= 1.4.3
8
8
 
9
9
  ## Installation
10
10
 
@@ -1,7 +1,7 @@
1
1
  module Backbone
2
2
  module Associations
3
3
  module Rails
4
- VERSION = '0.4.0'
4
+ VERSION = '0.4.1'
5
5
  end
6
6
  end
7
7
  end
@@ -1,34 +1,39 @@
1
1
  //
2
- // Backbone-associations.js 0.4.0
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
- _ = window._;
26
- Backbone = window.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 circular model graph.
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
- return this.getAttr.apply(this, arguments);
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
- var pathTokens = getPathArray(attr), initials = _.initial(pathTokens), last = _.last(pathTokens),
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 ((!parentModel && _.size(initials) > 0) || parentModel instanceof BackboneCollection) continue;
75
- parentModel instanceof AssociatedModel && (root = parentModel);
76
- obj = modelMap[root.cid] || (modelMap[root.cid] = {'model':root, 'data':{}});
77
- obj.data[last] = attributes[attr];
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
- result = this.setAttr.call(this, attributes, options);
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 processedRelations, tbp, attr;
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
- ModelProto.set.call(this, relationKey, val, relationOptions);
128
- } else if (!this.attributes[relationKey]) {
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].reset(val, relationOptions);
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
- ModelProto.set.call(this, relationKey, data, relationOptions)
150
+ attributes[relationKey] = data;
140
151
  }
141
152
 
142
- relationValue = this.attributes[relationKey];
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, tbp, options);
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
- if (_.contains(defaultEvents, eventType)) {
191
- _.size(opt) > 1 && (eventPath = opt[1]);
192
- // Find the specific object in the collection which has changed.
193
- if (relationValue instanceof BackboneCollection && "change" === eventType && eventObject) {
194
- //indexEventObject = _.indexOf(relationValue.models, eventObject);
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
- colModel = relationValue.find(function (model) {
199
- var changedModel = model.get(pathTokens);
200
- return eventObject === (changedModel instanceof AssociatedModel
201
- || changedModel instanceof BackboneCollection)
202
- ? changedModel : (model.get(initialTokens) || model);
203
- });
204
- colModel && (indexEventObject = relationValue.indexOf(colModel));
205
- }
206
- // Manipulate `eventPath`.
207
- eventPath = relationKey + (indexEventObject !== -1 ?
208
- "[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");
209
- args[0] = eventType + ":" + eventPath;
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
- // If event has been already triggered as result of same source `eventPath`,
212
- // no need to re-trigger event to prevent cycle.
213
- if (_proxyCalls) {
214
- eventAvailable = _.find(_proxyCalls, function (value, eventKey) {
215
- return eventPath.indexOf(eventKey, eventPath.length - eventKey.length) !== -1;
216
- });
217
- if (eventAvailable) return this;
218
- } else {
219
- _proxyCalls = relationValue._proxyCalls = {};
220
- }
221
- // Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
222
- _proxyCalls[eventPath] = true;
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
- // Get `reduced` result using passed `path` array or string.
375
- getAttr:function (path, iterator) {
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
- result = iterator.call(this, result, key, attrs);
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 _index = /^\d+$/;
394
- var _pathTokenizer = /[^\.\[\]]+/g;
291
+ var delimiters = /[^\.\[\]]+/g;
395
292
 
396
- // Get Path `attrs` as Array
397
- var getPathArray = function (path, iterator, context) {
398
- if (_.isString(path)) {
399
- iterator || (iterator = function (value) {
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);
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backbone-associations-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: