backbone-associations-rails 0.4.2 → 0.5.0

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/Rakefile CHANGED
@@ -44,11 +44,12 @@ namespace :backbone_associations do
44
44
  exit
45
45
  end
46
46
 
47
- # Update marionette
47
+ # Update backbone-associations
48
48
  puts "Updating backbone-associations..."
49
- url = "https://raw.github.com/dhruvaray/backbone-associations/#{sha}/lib/backbone-associations.js"
49
+ base_url = "https://raw.github.com/dhruvaray/backbone-associations/#{sha}"
50
+ files = %w{backbone-associations.js backbone-associations-min.js}
50
51
  Dir.chdir './vendor/assets/javascripts' do
51
- `curl -O #{url}`
52
+ files.each {|file| `curl -O #{base_url}/#{file}`}
52
53
  end
53
54
 
54
55
  # Update version file
@@ -1,7 +1,7 @@
1
1
  module Backbone
2
2
  module Associations
3
3
  module Rails
4
- VERSION = '0.4.2'
4
+ VERSION = '0.5.0'
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,11 @@
1
+ (function(){var v=this,g,h,w,m,r,s,z,o,A,B;"undefined"===typeof window?(g=require("underscore"),h=require("backbone"),"undefined"!==typeof exports&&(exports=module.exports=h)):(g=v._,h=v.Backbone);w=h.Model;m=h.Collection;r=w.prototype;s=m.prototype;A=/[\.\[\]]+/g;z="change add remove reset sort destroy".split(" ");B=["reset","sort"];h.Associations={VERSION:"0.5.0"};h.Associations.Many=h.Many="Many";h.Associations.One=h.One="One";o=h.AssociatedModel=h.Associations.AssociatedModel=w.extend({relations:void 0,
2
+ _proxyCalls:void 0,get:function(a){var c=r.get.call(this,a);return c?c:this._getAttr.apply(this,arguments)},set:function(a,c,d){var b;if(g.isObject(a)||a==null){b=a;d=c}else{b={};b[a]=c}a=this._set(b,d);this._processPendingEvents();return a},_set:function(a,c){var d,b,n,f,j=this;if(!a)return this;for(d in a){b||(b={});if(d.match(A)){var k=x(d);f=g.initial(k);k=k[k.length-1];f=this.get(f);if(f instanceof o){f=b[f.cid]||(b[f.cid]={model:f,data:{}});f.data[k]=a[d]}}else{f=b[this.cid]||(b[this.cid]={model:this,
3
+ data:{}});f.data[d]=a[d]}}if(b)for(n in b){f=b[n];this._setAttr.call(f.model,f.data,c)||(j=false)}else j=this._setAttr.call(this,a,c);return j},_setAttr:function(a,c){var d;c||(c={});if(c.unset)for(d in a)a[d]=void 0;this.parents=this.parents||[];this.relations&&g.each(this.relations,function(b){var d=b.key,f=b.relatedModel,j=b.collectionType,k=b.map,i=this.attributes[d],y=i&&i.idAttribute,e,q,l,p;f&&g.isString(f)&&(f=t(f));j&&g.isString(j)&&(j=t(j));k&&g.isString(k)&&(k=t(k));q=b.options?g.extend({},
4
+ b.options,c):c;if(a[d]){e=g.result(a,d);e=k?k(e):e;if(b.type===h.Many){if(j&&!j.prototype instanceof m)throw Error("collectionType must inherit from Backbone.Collection");if(e instanceof m)l=e;else if(i){i._deferEvents=true;i.set(e,c);l=i}else{l=j?new j:this._createCollection(f);l.add(e,q)}}else if(b.type===h.One&&f)if(e instanceof o)l=e;else if(i)if(i&&e[y]&&i.get(y)===e[y]){i._deferEvents=true;i._set(e,c);l=i}else l=new f(e,q);else l=new f(e,q);if((p=a[d]=l)&&!p._proxyCallback){p._proxyCallback=
5
+ function(){return this._bubbleEvent.call(this,d,p,arguments)};p.on("all",p._proxyCallback,this)}}if(a.hasOwnProperty(d)){b=a[d];f=this.attributes[d];if(b){b.parents=b.parents||[];g.indexOf(b.parents,this)==-1&&b.parents.push(this)}else if(f&&f.parents.length>0)f.parents=g.difference(f.parents,[this])}},this);return r.set.call(this,a,c)},_bubbleEvent:function(a,c,d){var b=d[0].split(":"),n=b[0],f=d[0]=="nested-change",j=d[1],k=d[2],i=-1,h=c._proxyCalls,e,q=g.indexOf(z,n)!==-1;if(!f){g.size(b)>1&&(e=
6
+ b[1]);g.indexOf(B,n)!==-1&&(k=j);if(c instanceof m&&q&&j){var l=x(e),p=g.initial(l);(b=c.find(function(a){if(j===a)return true;if(!a)return false;var b=a.get(p);if((b instanceof o||b instanceof m)&&j===b)return true;b=a.get(l);if((b instanceof o||b instanceof m)&&j===b||b instanceof m&&k&&k===b)return true}))&&(i=c.indexOf(b))}e=a+(i!==-1&&(n==="change"||e)?"["+i+"]":"")+(e?"."+e:"");if(/\[\*\]/g.test(e))return this;b=e.replace(/\[\d+\]/g,"[*]");i=[];i.push.apply(i,d);i[0]=n+":"+e;h=c._proxyCalls=
7
+ h||{};if(this._isEventAvailable.call(this,h,e))return this;h[e]=true;if("change"===n){this._previousAttributes[a]=c._previousAttributes;this.changed[a]=c}this.trigger.apply(this,i);"change"===n&&this.get(e)!=d[2]&&this.trigger.apply(this,["nested-change",e,d[1]]);h&&e&&delete h[e];if(e!==b){i[0]=n+":"+b;this.trigger.apply(this,i)}return this}},_isEventAvailable:function(a,c){return g.find(a,function(a,b){return c.indexOf(b,c.length-b.length)!==-1})},_createCollection:function(a){var c=a;g.isString(c)&&
8
+ (c=t(c));if(c&&c.prototype instanceof o){a=new m;a.model=c}else throw Error("type must inherit from Backbone.AssociatedModel");return a},_processPendingEvents:function(){if(!this.visited){this.visited=true;this._deferEvents=false;g.each(this._pendingEvents,function(a){a.c.trigger.apply(a.c,a.a)});this._pendingEvents=[];g.each(this.relations,function(a){(a=this.attributes[a.key])&&a._processPendingEvents()},this);delete this.visited}},trigger:function(a){if(this._deferEvents){this._pendingEvents=this._pendingEvents||
9
+ [];this._pendingEvents.push({c:this,a:arguments})}else r.trigger.apply(this,arguments)},toJSON:function(a){var c,d;if(!this.visited){this.visited=true;c=r.toJSON.apply(this,arguments);this.relations&&g.each(this.relations,function(b){var h=this.attributes[b.key];if(h){d=h.toJSON(a);c[b.key]=g.isArray(d)?g.compact(d):d}},this);delete this.visited}return c},clone:function(){return new this.constructor(this.toJSON())},_getAttr:function(a){var c=this,a=x(a),d,b;if(!(g.size(a)<1)){for(b=0;b<a.length;b++){d=
10
+ a[b];if(!c)break;c=c instanceof m?isNaN(d)?void 0:c.at(d):c.attributes[d]}return c}}});var C=/[^\.\[\]]+/g,x=function(a){return a===""?[""]:g.isString(a)?a.match(C):a||[]},t=function(a){return g.reduce(a.split("."),function(a,d){return a[d]},v)},D=function(a,c,d){var b;g.find(a,function(a){if(b=g.find(a.relations,function(b){return a.get(b.key)===c},this))return true},this);return b&&b.map?b.map(d):d},u={};g.each(["set","remove","reset"],function(a){u[a]=m.prototype[a];s[a]=function(c,d){this.model.prototype instanceof
11
+ o&&this.parents&&(arguments[0]=D(this.parents,this,c));return u[a].apply(this,arguments)}});u.trigger=s.trigger;s.trigger=function(a){if(this._deferEvents){this._pendingEvents=this._pendingEvents||[];this._pendingEvents.push({c:this,a:arguments})}else u.trigger.apply(this,arguments)};s._processPendingEvents=o.prototype._processPendingEvents}).call(this);
@@ -1,5 +1,5 @@
1
1
  //
2
- // Backbone-associations.js 0.4.2
2
+ // Backbone-associations.js 0.5.0
3
3
  //
4
4
  // (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd.
5
5
  // Backbone-associations may be freely distributed under the MIT license.
@@ -19,12 +19,15 @@
19
19
  // The top-level namespace. All public Backbone classes and modules will be attached to this.
20
20
  // Exported for the browser and CommonJS.
21
21
  var _, Backbone, BackboneModel, BackboneCollection, ModelProto,
22
- defaultEvents, AssociatedModel, pathChecker;
22
+ CollectionProto, defaultEvents, AssociatedModel, pathChecker,
23
+ collectionEvents;
23
24
 
24
- if (typeof require !== 'undefined') {
25
+ if (typeof window === 'undefined') {
25
26
  _ = require('underscore');
26
27
  Backbone = require('backbone');
27
- exports = module.exports = Backbone;
28
+ if (typeof exports !== 'undefined') {
29
+ exports = module.exports = Backbone;
30
+ }
28
31
  } else {
29
32
  _ = root._;
30
33
  Backbone = root.Backbone;
@@ -33,20 +36,25 @@
33
36
  BackboneModel = Backbone.Model;
34
37
  BackboneCollection = Backbone.Collection;
35
38
  ModelProto = BackboneModel.prototype;
39
+ CollectionProto = BackboneCollection.prototype;
36
40
  pathChecker = /[\.\[\]]+/g;
37
41
 
38
42
  // Built-in Backbone `events`.
39
- defaultEvents = ["change", "add", "remove", "reset", "destroy",
40
- "sync", "error", "sort", "request"];
43
+ defaultEvents = ["change", "add", "remove", "reset", "sort", "destroy"];
44
+ collectionEvents = ["reset", "sort"];
45
+
46
+ Backbone.Associations = {
47
+ VERSION:"0.5.0"
48
+ };
41
49
 
42
50
  // Backbone.AssociatedModel
43
51
  // --------------
44
52
 
45
53
  //Add `Many` and `One` relations to Backbone Object.
46
- Backbone.Many = "Many";
47
- Backbone.One = "One";
54
+ Backbone.Associations.Many = Backbone.Many = "Many";
55
+ Backbone.Associations.One = Backbone.One = "One";
48
56
  // Define `AssociatedModel` (Extends Backbone.Model).
49
- AssociatedModel = Backbone.AssociatedModel = BackboneModel.extend({
57
+ AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({
50
58
  // Define relations with Associated Model.
51
59
  relations:undefined,
52
60
  // Define `Model` property which can keep track of already fired `events`,
@@ -56,12 +64,12 @@
56
64
  // Get the value of an attribute.
57
65
  get:function (attr) {
58
66
  var obj = ModelProto.get.call(this, attr);
59
- return obj ? obj : this.getAttr.apply(this, arguments);
67
+ return obj ? obj : this._getAttr.apply(this, arguments);
60
68
  },
61
69
 
62
70
  // Set a hash of model attributes on the Backbone Model.
63
71
  set:function (key, value, options) {
64
- var attributes, attr, modelMap, modelId, obj, result = this;
72
+ var attributes, result;
65
73
  // Duplicate backbone's behavior to allow separate key/value parameters,
66
74
  // instead of a single 'attributes' object.
67
75
  if (_.isObject(key) || key == null) {
@@ -71,12 +79,23 @@
71
79
  attributes = {};
72
80
  attributes[key] = value;
73
81
  }
82
+ result = this._set(attributes, options);
83
+ // Trigger events which have been blocked until the entire object graph is updated.
84
+ this._processPendingEvents();
85
+ return result;
86
+
87
+ },
88
+
89
+ // Works with an attribute hash and options + fully qualified paths
90
+ _set:function (attributes, options) {
91
+ var attr, modelMap, modelId, obj, result = this;
74
92
  if (!attributes) return this;
75
93
  for (attr in attributes) {
76
94
  //Create a map for each unique object whose attributes we want to set
77
95
  modelMap || (modelMap = {});
78
96
  if (attr.match(pathChecker)) {
79
- var pathTokens = getPathArray(attr), initials = _.initial(pathTokens), last = pathTokens[pathTokens.length - 1],
97
+ var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
98
+ last = pathTokens[pathTokens.length - 1],
80
99
  parentModel = this.get(initials);
81
100
  if (parentModel instanceof AssociatedModel) {
82
101
  obj = modelMap[parentModel.cid] || (modelMap[parentModel.cid] = {'model':parentModel, 'data':{}});
@@ -87,26 +106,30 @@
87
106
  obj.data[attr] = attributes[attr];
88
107
  }
89
108
  }
109
+
90
110
  if (modelMap) {
91
111
  for (modelId in modelMap) {
92
112
  obj = modelMap[modelId];
93
- this.setAttr.call(obj.model, obj.data, options) || (result = false);
113
+ this._setAttr.call(obj.model, obj.data, options) || (result = false);
114
+
94
115
  }
95
116
  } else {
96
- return this.setAttr.call(this, attributes, options);
117
+ result = this._setAttr.call(this, attributes, options);
97
118
  }
98
119
  return result;
120
+
99
121
  },
100
122
 
101
123
  // Set a hash of model attributes on the object,
102
124
  // fire Backbone `event` with options.
103
125
  // It maintains relations between models during the set operation.
104
126
  // It also bubbles up child events to the parent.
105
- setAttr:function (attributes, options) {
127
+ _setAttr:function (attributes, options) {
106
128
  var attr;
107
129
  // Extract attributes and options.
108
130
  options || (options = {});
109
131
  if (options.unset) for (attr in attributes) attributes[attr] = void 0;
132
+ this.parents = this.parents || [];
110
133
 
111
134
  if (this.relations) {
112
135
  // Iterate over `this.relations` and `set` model and collection values
@@ -114,17 +137,26 @@
114
137
  _.each(this.relations, function (relation) {
115
138
  var relationKey = relation.key, relatedModel = relation.relatedModel,
116
139
  collectionType = relation.collectionType,
140
+ map = relation.map,
141
+ currVal = this.attributes[relationKey],
142
+ idKey = currVal && currVal.idAttribute,
117
143
  val, relationOptions, data, relationValue;
144
+
145
+ //Get class if relation and map is stored as a string.
146
+ relatedModel && _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel));
147
+ collectionType && _.isString(collectionType) && (collectionType = map2Scope(collectionType));
148
+ map && _.isString(map) && (map = map2Scope(map));
149
+ // Merge in `options` specific to this relation.
150
+ relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
151
+
118
152
  if (attributes[relationKey]) {
119
- //Get value of attribute with relation key in `val`.
153
+ // Get value of attribute with relation key in `val`.
120
154
  val = _.result(attributes, relationKey);
121
- // Get class if relation is stored as a string.
122
- relatedModel && _.isString(relatedModel) && (relatedModel = eval(relatedModel));
123
- collectionType && _.isString(collectionType) && (collectionType = eval(collectionType));
124
- // Merge in `options` specific to this relation.
125
- relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
155
+ // Map `val` if a transformation function is provided.
156
+ val = map ? map(val) : val;
157
+
126
158
  // If `relation.type` is `Backbone.Many`,
127
- // create `Backbone.Collection` with passed data and perform Backbone `set`.
159
+ // Create `Backbone.Collection` with passed data and perform Backbone `set`.
128
160
  if (relation.type === Backbone.Many) {
129
161
  // `collectionType` of defined `relation` should be instance of `Backbone.Collection`.
130
162
  if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
@@ -133,18 +165,46 @@
133
165
 
134
166
  if (val instanceof BackboneCollection) {
135
167
  data = val;
136
- attributes[relationKey] = data;
137
168
  } else {
138
- data = collectionType ? new collectionType() : this._createCollection(relatedModel);
139
- data.add(val, relationOptions);
140
- attributes[relationKey] = data;
169
+ // Create a new collection
170
+ if (!currVal) {
171
+ data = collectionType ? new collectionType() : this._createCollection(relatedModel);
172
+ data.add(val, relationOptions);
173
+ } else {
174
+ // Setting this flag will prevent events from firing immediately. That way clients
175
+ // will not get events until the entire object graph is updated.
176
+ currVal._deferEvents = true;
177
+ // Use Backbone.Collection's smart `set` method
178
+ currVal.set(val, options);
179
+ data = currVal;
180
+ }
141
181
  }
142
182
 
143
183
  } else if (relation.type === Backbone.One && relatedModel) {
144
- data = val instanceof AssociatedModel ? val : new relatedModel(val);
145
- attributes[relationKey] = data;
184
+ if (val instanceof AssociatedModel) {
185
+ data = val;
186
+ } else {
187
+ //Create a new model
188
+ if (!currVal) {
189
+ data = new relatedModel(val, relationOptions);
190
+ } else {
191
+ //Is the passed in data for the same key?
192
+ if (currVal && val[idKey] && currVal.get(idKey) === val[idKey]) {
193
+ // Setting this flag will prevent events from firing immediately. That way clients
194
+ // will not get events until the entire object graph is updated.
195
+ currVal._deferEvents = true;
196
+ // Perform the traditional `set` operation
197
+ currVal._set(val, options);
198
+ data = currVal;
199
+ } else {
200
+ data = new relatedModel(val, relationOptions);
201
+ }
202
+ }
203
+ }
204
+
146
205
  }
147
206
 
207
+ attributes[relationKey] = data;
148
208
  relationValue = data;
149
209
 
150
210
  // Add proxy events to respective parents.
@@ -157,80 +217,134 @@
157
217
  }
158
218
 
159
219
  }
220
+ //Distinguish between the value of undefined versus a set no-op
221
+ if (attributes.hasOwnProperty(relationKey)) {
222
+ //Maintain reverse pointers - a.k.a parents
223
+ var updated = attributes[relationKey];
224
+ var original = this.attributes[relationKey];
225
+ if (updated) {
226
+ updated.parents = updated.parents || [];
227
+ (_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
228
+ } else if (original && original.parents.length > 0) {
229
+ original.parents = _.difference(original.parents, [this]);
230
+ }
231
+ }
160
232
  }, this);
161
233
  }
162
234
  // Return results for `BackboneModel.set`.
163
- return ModelProto.set.call(this, attributes, options);
235
+ return ModelProto.set.call(this, attributes, options);
164
236
  },
165
237
  // Bubble-up event to `parent` Model
166
238
  _bubbleEvent:function (relationKey, relationValue, eventArguments) {
167
239
  var args = eventArguments,
168
240
  opt = args[0].split(":"),
169
241
  eventType = opt[0],
242
+ catch_all = args[0] == "nested-change",
170
243
  eventObject = args[1],
244
+ colObject = args[2],
171
245
  indexEventObject = -1,
172
246
  _proxyCalls = relationValue._proxyCalls,
247
+ cargs,
173
248
  eventPath,
174
- eventAvailable;
249
+ basecolEventPath,
250
+ isDefaultEvent = _.indexOf(defaultEvents, eventType) !== -1;
251
+
252
+ //Short circuit the listen in to the nested-graph event
253
+ if (catch_all) return;
254
+
175
255
  // Change the event name to a fully qualified path.
176
256
  _.size(opt) > 1 && (eventPath = opt[1]);
257
+
258
+ if (_.indexOf(collectionEvents, eventType) !== -1) {
259
+ colObject = eventObject;
260
+ }
261
+
177
262
  // Find the specific object in the collection which has changed.
178
- if (relationValue instanceof BackboneCollection && "change" === eventType && eventObject) {
263
+ if (relationValue instanceof BackboneCollection && isDefaultEvent && eventObject) {
179
264
  var pathTokens = getPathArray(eventPath),
180
265
  initialTokens = _.initial(pathTokens), colModel;
181
266
 
182
267
  colModel = relationValue.find(function (model) {
183
268
  if (eventObject === model) return true;
184
- if (model) {
185
- var changedModel = model.get(initialTokens);
186
- if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection) && eventObject === changedModel) return true;
187
- changedModel = model.get(pathTokens);
188
- return ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection) && eventObject === changedModel);
189
- }
190
- return false;
269
+ if (!model) return false;
270
+ var changedModel = model.get(initialTokens);
271
+
272
+ if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection)
273
+ && eventObject === changedModel)
274
+ return true;
275
+
276
+ changedModel = model.get(pathTokens);
277
+
278
+ if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection)
279
+ && eventObject === changedModel)
280
+ return true;
281
+
282
+ if (changedModel instanceof BackboneCollection && colObject
283
+ && colObject === changedModel)
284
+ return true;
191
285
  });
192
286
  colModel && (indexEventObject = relationValue.indexOf(colModel));
193
287
  }
288
+
194
289
  // Manipulate `eventPath`.
195
- eventPath = relationKey + (indexEventObject !== -1 ?
290
+ eventPath = relationKey + ((indexEventObject !== -1 && (eventType === "change" || eventPath)) ?
196
291
  "[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");
197
- args[0] = eventType + ":" + eventPath;
292
+
293
+ // Short circuit collection * events
294
+ if (/\[\*\]/g.test(eventPath)) return this;
295
+ basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
296
+
297
+ cargs = [];
298
+ cargs.push.apply(cargs, args);
299
+ cargs[0] = eventType + ":" + eventPath;
198
300
 
199
301
  // If event has been already triggered as result of same source `eventPath`,
200
302
  // no need to re-trigger event to prevent cycle.
201
- if (_proxyCalls) {
202
- eventAvailable = _.find(_proxyCalls, function (value, eventKey) {
203
- return eventPath.indexOf(eventKey, eventPath.length - eventKey.length) !== -1;
204
- });
205
- if (eventAvailable) return this;
206
- } else {
207
- _proxyCalls = relationValue._proxyCalls = {};
208
- }
303
+ _proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
304
+ if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;
305
+
209
306
  // Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
210
307
  _proxyCalls[eventPath] = true;
211
308
 
212
-
213
- //Set up previous attributes correctly. Backbone v0.9.10 upwards...
309
+ // Set up previous attributes correctly.
214
310
  if ("change" === eventType) {
215
311
  this._previousAttributes[relationKey] = relationValue._previousAttributes;
216
312
  this.changed[relationKey] = relationValue;
217
313
  }
218
314
 
219
315
  // Bubble up event to parent `model` with new changed arguments.
220
- this.trigger.apply(this, args);
316
+ this.trigger.apply(this, cargs);
317
+
318
+ //Only fire for change. Not change:attribute
319
+ if ("change" === eventType && this.get(eventPath) != args[2]) {
320
+ this.trigger.apply(this, ["nested-change", eventPath, args[1]]);
321
+ }
221
322
 
222
323
  // Remove `eventPath` from `_proxyCalls`,
223
- // if `eventPath` and `_proxCalls` are available,
324
+ // if `eventPath` and `_proxyCalls` are available,
224
325
  // which allow event to be triggered on for next operation of `set`.
225
- if (eventPath && _proxyCalls) {
226
- delete _proxyCalls[eventPath];
326
+ if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];
327
+
328
+ // Create a collection modified event with wild-card
329
+ if (eventPath !== basecolEventPath) {
330
+ cargs[0] = eventType + ":" + basecolEventPath;
331
+ this.trigger.apply(this, cargs);
227
332
  }
333
+
228
334
  return this;
229
335
  },
336
+
337
+ // Has event been fired from this source. Used to prevent event recursion in cyclic graphs
338
+ _isEventAvailable:function (_proxyCalls, path) {
339
+ return _.find(_proxyCalls, function (value, eventKey) {
340
+ return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
341
+ });
342
+ },
343
+
230
344
  // Returns New `collection` of type `relation.relatedModel`.
231
345
  _createCollection:function (type) {
232
346
  var collection, relatedModel = type;
233
- _.isString(relatedModel) && (relatedModel = eval(relatedModel));
347
+ _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel));
234
348
  // Creates new `Backbone.Collection` and defines model class.
235
349
  if (relatedModel && relatedModel.prototype instanceof AssociatedModel) {
236
350
  collection = new BackboneCollection();
@@ -240,6 +354,43 @@
240
354
  }
241
355
  return collection;
242
356
  },
357
+
358
+ // Process all pending events after the entire object graph has been updated
359
+ _processPendingEvents:function () {
360
+ if (!this.visited) {
361
+ this.visited = true;
362
+
363
+ this._deferEvents = false;
364
+
365
+ // Trigger all pending events
366
+ _.each(this._pendingEvents, function (e) {
367
+ e.c.trigger.apply(e.c, e.a);
368
+ });
369
+
370
+ this._pendingEvents = [];
371
+
372
+ // Traverse down the object graph and call process pending events on sub-trees
373
+ _.each(this.relations, function (relation) {
374
+ var val = this.attributes[relation.key];
375
+ val && val._processPendingEvents();
376
+ }, this);
377
+
378
+ delete this.visited;
379
+ }
380
+ },
381
+
382
+ // Override trigger to defer events in the object graph.
383
+ trigger:function (name) {
384
+ // Defer event processing
385
+ if (this._deferEvents) {
386
+ this._pendingEvents = this._pendingEvents || [];
387
+ // Maintain a queue of pending events to trigger after the entire object graph is updated.
388
+ this._pendingEvents.push({c:this, a:arguments});
389
+ } else {
390
+ ModelProto.trigger.apply(this, arguments);
391
+ }
392
+ },
393
+
243
394
  // The JSON representation of the model.
244
395
  toJSON:function (options) {
245
396
  var json, aJson;
@@ -268,8 +419,8 @@
268
419
  return new this.constructor(this.toJSON());
269
420
  },
270
421
 
271
- //Navigate the path to the leaf object in the path to query for the attribute value
272
- getAttr:function (path) {
422
+ // Navigate the path to the leaf object in the path to query for the attribute value
423
+ _getAttr:function (path) {
273
424
 
274
425
  var result = this,
275
426
  //Tokenize the path
@@ -281,7 +432,9 @@
281
432
  key = attrs[i];
282
433
  if (!result) break;
283
434
  //Navigate the path to get to the result
284
- result = result instanceof BackboneCollection && (!isNaN(key)) ? result.at(key) : result.attributes[key];
435
+ result = result instanceof BackboneCollection
436
+ ? (isNaN(key) ? undefined : result.at(key))
437
+ : result.attributes[key];
285
438
  }
286
439
  return result;
287
440
  }
@@ -293,5 +446,62 @@
293
446
  var getPathArray = function (path) {
294
447
  if (path === '') return [''];
295
448
  return _.isString(path) ? (path.match(delimiters)) : path || [];
296
- }
297
- }).call(this);
449
+ };
450
+
451
+ var map2Scope = function (path) {
452
+ return _.reduce(path.split('.'), function (memo, elem) {
453
+ return memo[elem]
454
+ }, root);
455
+ };
456
+
457
+ //Infer the relation from the collection's parents and find the appropriate map for the passed in `models`
458
+ var map2models = function (parents, target, models) {
459
+ var relation;
460
+ //Iterate over collection's parents
461
+ _.find(parents, function (parent) {
462
+ //Iterate over relations
463
+ relation = _.find(parent.relations, function (rel) {
464
+ return parent.get(rel.key) === target;
465
+ }, this);
466
+ if (relation) return true;//break;
467
+ }, this);
468
+
469
+ //If we found a relation and it has a mapping function
470
+ if (relation && relation.map) {
471
+ return relation.map(models)
472
+ }
473
+ return models;
474
+ };
475
+
476
+ var proxies = {};
477
+ // Proxy Backbone collection methods
478
+ _.each(['set', 'remove', 'reset'], function (method) {
479
+ proxies[method] = BackboneCollection.prototype[method];
480
+
481
+ CollectionProto[method] = function (models, options) {
482
+ //Short-circuit if this collection doesn't hold `AssociatedModels`
483
+ if (this.model.prototype instanceof AssociatedModel && this.parents) {
484
+ //Find a map function if available and perform a transformation
485
+ arguments[0] = map2models(this.parents, this, models);
486
+ }
487
+ return proxies[method].apply(this, arguments);
488
+ }
489
+ });
490
+
491
+ // Override trigger to defer events in the object graph.
492
+ proxies['trigger'] = CollectionProto['trigger'];
493
+ CollectionProto['trigger'] = function (name) {
494
+ if (this._deferEvents) {
495
+ this._pendingEvents = this._pendingEvents || [];
496
+ // Maintain a queue of pending events to trigger after the entire object graph is updated.
497
+ this._pendingEvents.push({c:this, a:arguments});
498
+ } else {
499
+ proxies['trigger'].apply(this, arguments);
500
+ }
501
+ };
502
+
503
+ // Attach process pending event functionality on collections as well. Re-use from `AssociatedModel`
504
+ CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
505
+
506
+
507
+ }).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.2
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-25 00:00:00.000000000 Z
12
+ date: 2013-06-20 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Backbone-associations provides a way of specifying 1:1 and 1:N relationships
15
15
  between Backbone models. Additionally, parent model instances (and objects extended
@@ -29,6 +29,7 @@ files:
29
29
  - backbone-associations-rails.gemspec
30
30
  - lib/backbone-associations-rails.rb
31
31
  - lib/backbone-associations-rails/version.rb
32
+ - vendor/assets/javascripts/backbone-associations-min.js
32
33
  - vendor/assets/javascripts/backbone-associations.js
33
34
  homepage: https://github.com/wingrunr21/backbone-associations-rails
34
35
  licenses: []