backbone-associations-rails 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: []