backbone-relational-rails 0.5.0 → 0.6.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/README.md CHANGED
@@ -18,7 +18,7 @@ Add the following directive to your Javascript manifest file (application.js):
18
18
 
19
19
  ## Versioning
20
20
 
21
- backbone-relational-rails 0.5.0 == Backbone-relational 0.5.0
21
+ backbone-relational-rails 0.6.0 == Backbone-relational 0.6.0
22
22
 
23
23
  Every attempt is made to mirror the currently shipping Backbone-relational version number wherever possible.
24
24
  The major, minor, and patch version numbers will always represent the Backbone-relational version. Should a gem
@@ -1,7 +1,7 @@
1
1
  module Backbone
2
2
  module Relational
3
3
  module Rails
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
6
6
  end
7
7
  end
@@ -1,12 +1,14 @@
1
- /**
2
- * Backbone-relational.js 0.5.0
1
+ /**
2
+ * Backbone-relational.js 0.6.0
3
3
  * (c) 2011 Paul Uithol
4
4
  *
5
- * Backbone-relational may be freely distributed under the MIT license.
5
+ * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.
6
6
  * For details and documentation: https://github.com/PaulUithol/Backbone-relational.
7
- * Depends on (as in, compeletely useless without) Backbone: https://github.com/documentcloud/backbone.
7
+ * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
8
8
  */
9
9
  ( function( undefined ) {
10
+ "use strict";
11
+
10
12
  /**
11
13
  * CommonJS shim
12
14
  **/
@@ -17,15 +19,15 @@
17
19
  exports = module.exports = Backbone;
18
20
  }
19
21
  else {
20
- var _ = window._;
22
+ _ = window._;
21
23
  Backbone = window.Backbone;
22
24
  exports = window;
23
25
  }
24
-
26
+
25
27
  Backbone.Relational = {
26
28
  showWarnings: true
27
29
  };
28
-
30
+
29
31
  /**
30
32
  * Semaphore mixin; can be used as both binary and counting.
31
33
  **/
@@ -114,37 +116,87 @@
114
116
  * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
115
117
  * Handles lookup for relations.
116
118
  */
117
- Backbone.Store = function() {
119
+ Backbone.Store = function() {
118
120
  this._collections = [];
119
121
  this._reverseRelations = [];
122
+ this._subModels = [];
123
+ this._modelScopes = [ exports ];
120
124
  };
121
125
  _.extend( Backbone.Store.prototype, Backbone.Events, {
122
- _collections: null,
123
- _reverseRelations: null,
126
+ addModelScope: function( scope ) {
127
+ this._modelScopes.push( scope );
128
+ },
129
+
130
+ /**
131
+ * Add a set of subModelTypes to the store, that can be used to resolve the '_superModel'
132
+ * for a model later in 'setupSuperModel'.
133
+ *
134
+ * @param {Backbone.RelationalModel} subModelTypes
135
+ * @param {Backbone.RelationalModel} superModelType
136
+ */
137
+ addSubModels: function( subModelTypes, superModelType ) {
138
+ this._subModels.push({
139
+ 'superModelType': superModelType,
140
+ 'subModels': subModelTypes
141
+ });
142
+ },
143
+
144
+ /**
145
+ * Check if the given modelType is registered as another model's subModel. If so, add it to the super model's
146
+ * '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'.
147
+ *
148
+ * @param {Backbone.RelationalModel} modelType
149
+ */
150
+ setupSuperModel: function( modelType ) {
151
+ _.find( this._subModels, function( subModelDef ) {
152
+ return _.find( subModelDef.subModels, function( subModelTypeName, typeValue ) {
153
+ var subModelType = this.getObjectByName( subModelTypeName );
154
+
155
+ if ( modelType === subModelType ) {
156
+ // Set 'modelType' as a child of the found superModel
157
+ subModelDef.superModelType._subModels[ typeValue ] = modelType;
158
+
159
+ // Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'.
160
+ modelType._superModel = subModelDef.superModelType;
161
+ modelType._subModelTypeValue = typeValue;
162
+ modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute;
163
+ return true;
164
+ }
165
+ }, this );
166
+ }, this );
167
+ },
124
168
 
125
169
  /**
126
170
  * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
127
171
  * existing instances of 'model' in the store as well.
128
- * @param {object} relation
172
+ * @param {Object} relation
129
173
  * @param {Backbone.RelationalModel} relation.model
130
174
  * @param {String} relation.type
131
175
  * @param {String} relation.key
132
- * @param {String|object} relation.relatedModel
176
+ * @param {String|Object} relation.relatedModel
133
177
  */
134
178
  addReverseRelation: function( relation ) {
135
179
  var exists = _.any( this._reverseRelations, function( rel ) {
136
- return _.all( relation, function( val, key ) {
137
- return val === rel[ key ];
180
+ return _.all( relation, function( val, key ) {
181
+ return val === rel[ key ];
182
+ });
138
183
  });
139
- });
140
184
 
141
185
  if ( !exists && relation.model && relation.type ) {
142
186
  this._reverseRelations.push( relation );
143
187
 
144
- if ( !relation.model.prototype.relations ) {
145
- relation.model.prototype.relations = [];
146
- }
147
- relation.model.prototype.relations.push( relation );
188
+ var addRelation = function( model, relation ) {
189
+ if ( !model.prototype.relations ) {
190
+ model.prototype.relations = [];
191
+ }
192
+ model.prototype.relations.push( relation );
193
+
194
+ _.each( model._subModels, function( subModel ) {
195
+ addRelation( subModel, relation );
196
+ }, this );
197
+ };
198
+
199
+ addRelation( relation.model, relation );
148
200
 
149
201
  this.retroFitRelation( relation );
150
202
  }
@@ -152,11 +204,15 @@
152
204
 
153
205
  /**
154
206
  * Add a 'relation' to all existing instances of 'relation.model' in the store
155
- * @param {object} relation
207
+ * @param {Object} relation
156
208
  */
157
209
  retroFitRelation: function( relation ) {
158
210
  var coll = this.getCollection( relation.model );
159
211
  coll.each( function( model ) {
212
+ if ( !( model instanceof relation.model ) ) {
213
+ return;
214
+ }
215
+
160
216
  new relation.type( model, relation );
161
217
  }, this);
162
218
  },
@@ -167,13 +223,21 @@
167
223
  * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
168
224
  */
169
225
  getCollection: function( model ) {
170
- var coll = _.detect( this._collections, function( c ) {
171
- // Check if model is the type itself (a ref to the constructor), or is of type c.model
172
- return model === c.model || model.constructor === c.model;
226
+ if ( model instanceof Backbone.RelationalModel ) {
227
+ model = model.constructor;
228
+ }
229
+
230
+ var rootModel = model;
231
+ while ( rootModel._superModel ) {
232
+ rootModel = rootModel._superModel;
233
+ }
234
+
235
+ var coll = _.detect( this._collections, function( c ) {
236
+ return c.model === rootModel;
173
237
  });
174
238
 
175
239
  if ( !coll ) {
176
- coll = this._createCollection( model );
240
+ coll = this._createCollection( rootModel );
177
241
  }
178
242
 
179
243
  return coll;
@@ -182,25 +246,35 @@
182
246
  /**
183
247
  * Find a type on the global object by name. Splits name on dots.
184
248
  * @param {String} name
185
- * @return {object}
249
+ * @return {Object}
186
250
  */
187
251
  getObjectByName: function( name ) {
188
- var type = _.reduce( name.split( '.' ), function( memo, val ) {
189
- return memo[ val ];
190
- }, exports);
191
- return type !== exports ? type: null;
252
+ var parts = name.split( '.' ),
253
+ type = null;
254
+
255
+ _.find( this._modelScopes, function( scope ) {
256
+ type = _.reduce( parts, function( memo, val ) {
257
+ return memo[ val ];
258
+ }, scope );
259
+
260
+ if ( type && type !== scope ) {
261
+ return true;
262
+ }
263
+ }, this );
264
+
265
+ return type;
192
266
  },
193
267
 
194
268
  _createCollection: function( type ) {
195
269
  var coll;
196
270
 
197
- // If 'type' is an instance, take it's constructor
271
+ // If 'type' is an instance, take its constructor
198
272
  if ( type instanceof Backbone.RelationalModel ) {
199
273
  type = type.constructor;
200
274
  }
201
275
 
202
276
  // Type should inherit from Backbone.RelationalModel.
203
- if ( type.prototype instanceof Backbone.RelationalModel.prototype.constructor ) {
277
+ if ( type.prototype instanceof Backbone.RelationalModel ) {
204
278
  coll = new Backbone.Collection();
205
279
  coll.model = type;
206
280
 
@@ -209,10 +283,53 @@
209
283
 
210
284
  return coll;
211
285
  },
212
-
213
- find: function( type, id ) {
286
+
287
+ /**
288
+ * Find the attribute that is to be used as the `id` on a given object
289
+ * @param type
290
+ * @param {String|Number|Object|Backbone.RelationalModel} item
291
+ * @return {String|Number}
292
+ */
293
+ resolveIdForItem: function( type, item ) {
294
+ var id = _.isString( item ) || _.isNumber( item ) ? item : null;
295
+
296
+ if ( id === null ) {
297
+ if ( item instanceof Backbone.RelationalModel ) {
298
+ id = item.id;
299
+ }
300
+ else if ( _.isObject( item ) ) {
301
+ id = item[ type.prototype.idAttribute ];
302
+ }
303
+ }
304
+
305
+ // Make all falsy values `null` (except for 0, which could be an id.. see '/issues/179')
306
+ if ( !id && id !== 0 ) {
307
+ id = null;
308
+ }
309
+
310
+ return id;
311
+ },
312
+
313
+ /**
314
+ *
315
+ * @param type
316
+ * @param {String|Number|Object|Backbone.RelationalModel} item
317
+ */
318
+ find: function( type, item ) {
319
+ var id = this.resolveIdForItem( type, item );
214
320
  var coll = this.getCollection( type );
215
- return coll && coll.get( id );
321
+
322
+ // Because the found object could be of any of the type's superModel
323
+ // types, only return it if it's actually of the type asked for.
324
+ if ( coll ) {
325
+ var obj = coll.get( id );
326
+
327
+ if ( obj instanceof type ) {
328
+ return obj;
329
+ }
330
+ }
331
+
332
+ return null;
216
333
  },
217
334
 
218
335
  /**
@@ -253,19 +370,19 @@
253
370
  * are used to regulate addition and removal of models from relations.
254
371
  *
255
372
  * @param {Backbone.RelationalModel} instance
256
- * @param {object} options
373
+ * @param {Object} options
257
374
  * @param {string} options.key
258
375
  * @param {Backbone.RelationalModel.constructor} options.relatedModel
259
- * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids.
376
+ * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids.
260
377
  * @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store.
261
- * @param {object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
378
+ * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
262
379
  * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
263
380
  * {Backbone.Relation|String} type ('HasOne' or 'HasMany').
264
381
  */
265
382
  Backbone.Relation = function( instance, options ) {
266
383
  this.instance = instance;
267
384
  // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
268
- options = ( typeof options === 'object' && options ) || {};
385
+ options = _.isObject( options ) ? options : {};
269
386
  this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
270
387
  this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
271
388
  Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
@@ -273,6 +390,8 @@
273
390
  this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
274
391
 
275
392
  this.key = this.options.key;
393
+ this.keySource = this.options.keySource || this.key;
394
+ this.keyDestination = this.options.keyDestination || this.keySource || this.key;
276
395
 
277
396
  // 'exports' should be the global object where 'relatedModel' can be found on if given as a string.
278
397
  this.relatedModel = this.options.relatedModel;
@@ -284,8 +403,14 @@
284
403
  return false;
285
404
  }
286
405
 
287
- if(instance) {
288
- this.keyContents = this.instance.get( this.key );
406
+ if ( instance ) {
407
+ this.keyContents = this.instance.get( this.keySource );
408
+
409
+ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
410
+ if ( this.key !== this.keySource ) {
411
+ this.instance.unset( this.keySource, { silent: true } );
412
+ }
413
+
289
414
  // Add this Relation to instance._relations
290
415
  this.instance._relations.push( this );
291
416
  }
@@ -304,7 +429,7 @@
304
429
 
305
430
  _.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' );
306
431
 
307
- if( instance ) {
432
+ if ( instance ) {
308
433
  this.initialize();
309
434
 
310
435
  // When a model in the store is destroyed, check if it is 'this.instance'.
@@ -358,64 +483,68 @@
358
483
  * @return {Boolean} True if pre-conditions are satisfied, false if they're not.
359
484
  */
360
485
  checkPreconditions: function() {
361
- var i = this.instance, k = this.key, m = this.model, rm = this.relatedModel, warn = Backbone.Relational.showWarnings;
486
+ var i = this.instance,
487
+ k = this.key,
488
+ m = this.model,
489
+ rm = this.relatedModel,
490
+ warn = Backbone.Relational.showWarnings && typeof console !== 'undefined';
491
+
362
492
  if ( !m || !k || !rm ) {
363
- warn && typeof console !== 'undefined' && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, k, rm );
493
+ warn && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, m, k, rm );
364
494
  return false;
365
495
  }
366
- // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
367
- if ( !( m.prototype instanceof Backbone.RelationalModel.prototype.constructor ) ) {
368
- warn && typeof console !== 'undefined' && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i );
496
+ // Check if the type in 'model' inherits from Backbone.RelationalModel
497
+ if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
498
+ warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i );
369
499
  return false;
370
500
  }
371
501
  // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
372
- if ( !( rm.prototype instanceof Backbone.RelationalModel.prototype.constructor ) ) {
373
- warn && typeof console !== 'undefined' && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
502
+ if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
503
+ warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
374
504
  return false;
375
505
  }
376
506
  // Check if this is not a HasMany, and the reverse relation is HasMany as well
377
- if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany.prototype.constructor ) {
378
- warn && typeof console !== 'undefined' && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
507
+ if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
508
+ warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
379
509
  return false;
380
510
  }
381
511
 
382
- if( i ) {
383
- // Check if we're not attempting to create a duplicate relationship
384
- if ( i._relations.length ) {
385
- var exists = _.any( i._relations, function( rel ) {
512
+ // Check if we're not attempting to create a duplicate relationship
513
+ if ( i && i._relations.length ) {
514
+ var exists = _.any( i._relations, function( rel ) {
386
515
  var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key;
387
516
  return rel.relatedModel === rm && rel.key === k &&
388
517
  ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key );
389
518
  }, this );
390
519
 
391
- if ( exists ) {
392
- warn && typeof console !== 'undefined' && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
520
+ if ( exists ) {
521
+ warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
393
522
  this, i, k, rm, this.reverseRelation.key );
394
- return false;
395
- }
523
+ return false;
396
524
  }
397
525
  }
526
+
398
527
  return true;
399
528
  },
400
-
529
+
530
+ /**
531
+ * Set the related model(s) for this relation
532
+ * @param {Backbone.Mode|Backbone.Collection} related
533
+ * @param {Object} [options]
534
+ */
401
535
  setRelated: function( related, options ) {
402
536
  this.related = related;
403
- var value = {};
404
- value[ this.key ] = related;
537
+
405
538
  this.instance.acquire();
406
- this.instance.set( value, _.defaults( options || {}, { silent: true } ) );
539
+ this.instance.set( this.key, related, _.defaults( options || {}, { silent: true } ) );
407
540
  this.instance.release();
408
541
  },
409
542
 
410
- createModel: function( item ) {
411
- if ( this.options.createModels && typeof( item ) === 'object' ) {
412
- return new this.relatedModel( item );
413
- }
414
- },
415
-
416
543
  /**
417
544
  * Determine if a relation (on a different RelationalModel) is the reverse
418
545
  * relation of the current one.
546
+ * @param {Backbone.Relation} relation
547
+ * @return {Boolean}
419
548
  */
420
549
  _isReverseRelation: function( relation ) {
421
550
  if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
@@ -429,34 +558,52 @@
429
558
  * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
430
559
  * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
431
560
  * If not specified, 'this.related' is used.
561
+ * @return {Backbone.Relation[]}
432
562
  */
433
563
  getReverseRelations: function( model ) {
434
564
  var reverseRelations = [];
435
565
  // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
436
566
  var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
437
567
  _.each( models , function( related ) {
438
- _.each( related.getRelations(), function( relation ) {
439
- if ( this._isReverseRelation( relation ) ) {
440
- reverseRelations.push( relation );
441
- }
568
+ _.each( related.getRelations(), function( relation ) {
569
+ if ( this._isReverseRelation( relation ) ) {
570
+ reverseRelations.push( relation );
571
+ }
572
+ }, this );
442
573
  }, this );
443
- }, this );
444
574
 
445
575
  return reverseRelations;
446
576
  },
447
577
 
448
578
  /**
449
- * Rename options.silent, so add/remove events propagate properly.
579
+ * Rename options.silent to options.silentChange, so events propagate properly.
450
580
  * (for example in HasMany, from 'addRelated'->'handleAddition')
581
+ * @param {Object} [options]
582
+ * @return {Object}
451
583
  */
452
584
  sanitizeOptions: function( options ) {
453
- options || ( options = {} );
585
+ options = options ? _.clone( options ) : {};
454
586
  if ( options.silent ) {
455
- options = _.extend( {}, options, { silentChange: true } );
587
+ options.silentChange = true;
456
588
  delete options.silent;
457
589
  }
458
590
  return options;
459
591
  },
592
+
593
+ /**
594
+ * Rename options.silentChange to options.silent, so events are silenced as intended in Backbone's
595
+ * original functions.
596
+ * @param {Object} [options]
597
+ * @return {Object}
598
+ */
599
+ unsanitizeOptions: function( options ) {
600
+ options = options ? _.clone( options ) : {};
601
+ if ( options.silentChange ) {
602
+ options.silent = true;
603
+ delete options.silentChange;
604
+ }
605
+ return options;
606
+ },
460
607
 
461
608
  // Cleanup. Get reverse relation, call removeRelated on each.
462
609
  destroy: function() {
@@ -487,10 +634,9 @@
487
634
  this.setRelated( model );
488
635
 
489
636
  // Notify new 'related' object of the new relation.
490
- var dit = this;
491
- _.each( dit.getReverseRelations(), function( relation ) {
492
- relation.addRelated( dit.instance );
493
- } );
637
+ _.each( this.getReverseRelations(), function( relation ) {
638
+ relation.addRelated( this.instance );
639
+ }, this );
494
640
  },
495
641
 
496
642
  findRelated: function( options ) {
@@ -500,16 +646,8 @@
500
646
  if ( item instanceof this.relatedModel ) {
501
647
  model = item;
502
648
  }
503
- else if ( item && ( _.isString( item ) || _.isNumber( item ) || typeof( item ) === 'object' ) ) {
504
- // Try to find an instance of the appropriate 'relatedModel' in the store, or create it
505
- var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
506
- model = Backbone.Relational.store.find( this.relatedModel, id );
507
- if ( model && _.isObject( item ) ) {
508
- model.set( item, options );
509
- }
510
- else if ( !model ) {
511
- model = this.createModel( item );
512
- }
649
+ else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
650
+ model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
513
651
  }
514
652
 
515
653
  return model;
@@ -519,7 +657,7 @@
519
657
  * If the key is changed, notify old & new reverse relations and initialize the new relation
520
658
  */
521
659
  onChange: function( model, attr, options ) {
522
- // Don't accept recursive calls to onChange (like onChange->findRelated->createModel->initializeRelations->addRelated->onChange)
660
+ // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
523
661
  if ( this.isLocked() ) {
524
662
  return;
525
663
  }
@@ -582,9 +720,9 @@
582
720
  options = this.sanitizeOptions( options );
583
721
 
584
722
  var item = this.keyContents;
585
- if ( item && ( _.isString( item ) || _.isNumber( item ) || typeof( item ) === 'object' ) ) {
586
- var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
587
- if ( model.id === id ) {
723
+ if ( item || item === 0 ) { // since 0 can be a valid `id` as well
724
+ var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
725
+ if ( !_.isNull( id ) && model.id === id ) {
588
726
  this.addRelated( model, options );
589
727
  }
590
728
  }
@@ -617,7 +755,8 @@
617
755
  options: {
618
756
  reverseRelation: { type: 'HasOne' },
619
757
  collectionType: Backbone.Collection,
620
- collectionKey: true
758
+ collectionKey: true,
759
+ collectionOptions: {}
621
760
  },
622
761
 
623
762
  initialize: function() {
@@ -626,37 +765,58 @@
626
765
 
627
766
  // Handle a custom 'collectionType'
628
767
  this.collectionType = this.options.collectionType;
629
- if ( _( this.collectionType ).isString() ) {
768
+ if ( _.isString( this.collectionType ) ) {
630
769
  this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
631
770
  }
632
- if ( !this.collectionType.prototype instanceof Backbone.Collection.prototype.constructor ){
771
+ if ( !this.collectionType.prototype instanceof Backbone.Collection ){
633
772
  throw new Error( 'collectionType must inherit from Backbone.Collection' );
634
773
  }
635
-
636
- this.setRelated( this.prepareCollection( new this.collectionType() ) );
774
+
775
+ // Handle cases where a model/relation is created with a collection passed straight into 'attributes'
776
+ if ( this.keyContents instanceof Backbone.Collection ) {
777
+ this.setRelated( this._prepareCollection( this.keyContents ) );
778
+ }
779
+ else {
780
+ this.setRelated( this._prepareCollection() );
781
+ }
782
+
637
783
  this.findRelated( { silent: true } );
638
784
  },
639
785
 
640
- prepareCollection: function( collection ) {
786
+ _getCollectionOptions: function() {
787
+ return _.isFunction( this.options.collectionOptions ) ?
788
+ this.options.collectionOptions( this.instance ) :
789
+ this.options.collectionOptions;
790
+ },
791
+
792
+ /**
793
+ * Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany.
794
+ * If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option.
795
+ * @param {Backbone.Collection} [collection]
796
+ */
797
+ _prepareCollection: function( collection ) {
641
798
  if ( this.related ) {
642
799
  this.related
643
800
  .unbind( 'relational:add', this.handleAddition )
644
801
  .unbind( 'relational:remove', this.handleRemoval )
645
802
  .unbind( 'relational:reset', this.handleReset )
646
803
  }
647
-
648
- collection.reset();
804
+
805
+ if ( !collection || !( collection instanceof Backbone.Collection ) ) {
806
+ collection = new this.collectionType( [], this._getCollectionOptions() );
807
+ }
808
+
649
809
  collection.model = this.relatedModel;
650
810
 
651
811
  if ( this.options.collectionKey ) {
652
812
  var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey;
653
813
 
654
- if (collection[ key ] && collection[ key ] !== this.instance ) {
814
+ if ( collection[ key ] && collection[ key ] !== this.instance ) {
655
815
  if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
656
816
  console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey );
657
817
  }
658
818
  }
659
- else {
819
+ else if ( key ) {
660
820
  collection[ key ] = this.instance;
661
821
  }
662
822
  }
@@ -671,25 +831,36 @@
671
831
 
672
832
  findRelated: function( options ) {
673
833
  if ( this.keyContents ) {
674
- // Handle cases the an API/user supplies just an Object/id instead of an Array
675
- this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ];
834
+ var models = [];
676
835
 
677
- // Try to find instances of the appropriate 'relatedModel' in the store
678
- _.each( this.keyContents, function( item ) {
679
- var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
680
-
681
- var model = Backbone.Relational.store.find( this.relatedModel, id );
682
- if ( model && _.isObject( item ) ) {
683
- model.set( item, options );
684
- }
685
- else if ( !model ) {
686
- model = this.createModel( item );
687
- }
688
-
689
- if ( model && !this.related.getByCid( model ) && !this.related.get( model ) ) {
690
- this.related.add( model );
691
- }
692
- }, this);
836
+ if ( this.keyContents instanceof Backbone.Collection ) {
837
+ models = this.keyContents.models;
838
+ }
839
+ else {
840
+ // Handle cases the an API/user supplies just an Object/id instead of an Array
841
+ this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ];
842
+
843
+ // Try to find instances of the appropriate 'relatedModel' in the store
844
+ _.each( this.keyContents, function( item ) {
845
+ var model = null;
846
+ if ( item instanceof this.relatedModel ) {
847
+ model = item;
848
+ }
849
+ else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
850
+ model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
851
+ }
852
+
853
+ if ( model && !this.related.getByCid( model ) && !this.related.get( model ) ) {
854
+ models.push( model );
855
+ }
856
+ }, this );
857
+ }
858
+
859
+ // Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.)
860
+ if ( models.length ) {
861
+ options = this.unsanitizeOptions( options );
862
+ this.related.add( models, options );
863
+ }
693
864
  }
694
865
  },
695
866
 
@@ -707,14 +878,24 @@
707
878
 
708
879
  // Replace 'this.related' by 'attr' if it is a Backbone.Collection
709
880
  if ( attr instanceof Backbone.Collection ) {
710
- this.prepareCollection( attr );
881
+ this._prepareCollection( attr );
711
882
  this.related = attr;
712
883
  }
713
884
  // Otherwise, 'attr' should be an array of related object ids.
714
- // Re-use the current 'this.related' if it is a Backbone.Collection.
885
+ // Re-use the current 'this.related' if it is a Backbone.Collection, and remove any current entries.
886
+ // Otherwise, create a new collection.
715
887
  else {
716
- var coll = this.related instanceof Backbone.Collection ? this.related : new this.collectionType();
717
- this.setRelated( this.prepareCollection( coll ) );
888
+ var coll;
889
+
890
+ if ( this.related instanceof Backbone.Collection ) {
891
+ coll = this.related;
892
+ coll.remove( coll.models );
893
+ }
894
+ else {
895
+ coll = this._prepareCollection();
896
+ }
897
+
898
+ this.setRelated( coll );
718
899
  this.findRelated( options );
719
900
  }
720
901
 
@@ -734,9 +915,9 @@
734
915
  if ( !this.related.getByCid( model ) && !this.related.get( model ) ) {
735
916
  // Check if this new model was specified in 'this.keyContents'
736
917
  var item = _.any( this.keyContents, function( item ) {
737
- var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
738
- return id && id === model.id;
739
- }, this );
918
+ var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
919
+ return !_.isNull( id ) && id === model.id;
920
+ }, this );
740
921
 
741
922
  if ( item ) {
742
923
  this.related.add( model, options );
@@ -752,7 +933,7 @@
752
933
  //console.debug('handleAddition called; args=%o', arguments);
753
934
  // Make sure the model is in fact a valid model before continuing.
754
935
  // (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel)
755
- if( !( model instanceof Backbone.Model ) ) {
936
+ if ( !( model instanceof Backbone.Model ) ) {
756
937
  return;
757
938
  }
758
939
 
@@ -775,7 +956,7 @@
775
956
  */
776
957
  handleRemoval: function( model, coll, options ) {
777
958
  //console.debug('handleRemoval called; args=%o', arguments);
778
- if( !( model instanceof Backbone.Model ) ) {
959
+ if ( !( model instanceof Backbone.Model ) ) {
779
960
  return;
780
961
  }
781
962
 
@@ -802,6 +983,7 @@
802
983
 
803
984
  addRelated: function( model, options ) {
804
985
  var dit = this;
986
+ options = this.unsanitizeOptions( options );
805
987
  model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice
806
988
  if ( dit.related && !dit.related.getByCid( model ) && !dit.related.get( model ) ) {
807
989
  dit.related.add( model, options );
@@ -810,6 +992,7 @@
810
992
  },
811
993
 
812
994
  removeRelated: function( model, options ) {
995
+ options = this.unsanitizeOptions( options );
813
996
  if ( this.related.getByCid( model ) || this.related.get( model ) ) {
814
997
  this.related.remove( model, options );
815
998
  }
@@ -830,6 +1013,9 @@
830
1013
  _deferProcessing: false,
831
1014
  _queue: null,
832
1015
 
1016
+ subModelTypeAttribute: 'type',
1017
+ subModelTypes: null,
1018
+
833
1019
  constructor: function( attributes, options ) {
834
1020
  // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
835
1021
  // Defer 'processQueue', so that when 'Relation.createModels' is used we:
@@ -861,7 +1047,7 @@
861
1047
  this._queue.block();
862
1048
  Backbone.Relational.eventQueue.block();
863
1049
 
864
- Backbone.Model.prototype.constructor.apply( this, arguments );
1050
+ Backbone.Model.apply( this, arguments );
865
1051
 
866
1052
  // Try to run the global queue holding external events
867
1053
  Backbone.Relational.eventQueue.unblock();
@@ -894,7 +1080,7 @@
894
1080
 
895
1081
  _.each( this.relations, function( rel ) {
896
1082
  var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
897
- if ( type && type.prototype instanceof Backbone.Relation.prototype.constructor ) {
1083
+ if ( type && type.prototype instanceof Backbone.Relation ) {
898
1084
  new type( this, rel ); // Also pushes the new Relation into _relations
899
1085
  }
900
1086
  else {
@@ -906,17 +1092,18 @@
906
1092
  this.release();
907
1093
  this.processQueue();
908
1094
  },
909
-
1095
+
910
1096
  /**
911
1097
  * When new values are set, notify this model's relations (also if options.silent is set).
912
1098
  * (Relation.setRelated locks this model before calling 'set' on it to prevent loops)
913
1099
  */
914
1100
  updateRelations: function( options ) {
915
- if( this._isInitialized && !this.isLocked() ) {
1101
+ if ( this._isInitialized && !this.isLocked() ) {
916
1102
  _.each( this._relations, function( rel ) {
917
- var val = this.attributes[ rel.key ];
1103
+ // Update from data in `rel.keySource` if set, or `rel.key` otherwise
1104
+ var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ];
918
1105
  if ( rel.related !== val ) {
919
- this.trigger('relational:change:' + rel.key, this, val, options || {} );
1106
+ this.trigger( 'relational:change:' + rel.key, this, val, options || {} );
920
1107
  }
921
1108
  }, this );
922
1109
  }
@@ -962,18 +1149,19 @@
962
1149
  /**
963
1150
  * Retrieve related objects.
964
1151
  * @param key {string} The relation key to fetch models for.
965
- * @param options {object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
1152
+ * @param options {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
1153
+ * @param update {boolean} Whether to force a fetch from the server (updating existing models).
966
1154
  * @return {jQuery.when[]} An array of request objects
967
1155
  */
968
- fetchRelated: function( key, options ) {
1156
+ fetchRelated: function( key, options, update ) {
969
1157
  options || ( options = {} );
970
1158
  var setUrl,
971
1159
  requests = [],
972
1160
  rel = this.getRelation( key ),
973
1161
  keyContents = rel && rel.keyContents,
974
1162
  toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) {
975
- var id = _.isString( item ) || _.isNumber( item ) ? item : item[ rel.relatedModel.prototype.idAttribute ];
976
- return id && !Backbone.Relational.store.find( rel.relatedModel, id );
1163
+ var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item );
1164
+ return !_.isNull( id ) && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) );
977
1165
  }, this );
978
1166
 
979
1167
  if ( toFetch && toFetch.length ) {
@@ -981,13 +1169,13 @@
981
1169
  var models = _.map( toFetch, function( item ) {
982
1170
  var model;
983
1171
 
984
- if ( typeof( item ) === 'object' ) {
985
- model = new rel.relatedModel( item );
1172
+ if ( _.isObject( item ) ) {
1173
+ model = rel.relatedModel.build( item );
986
1174
  }
987
1175
  else {
988
1176
  var attrs = {};
989
1177
  attrs[ rel.relatedModel.prototype.idAttribute ] = item;
990
- model = new rel.relatedModel( attrs );
1178
+ model = rel.relatedModel.build( attrs );
991
1179
  }
992
1180
 
993
1181
  return model;
@@ -1007,9 +1195,9 @@
1007
1195
  error: function() {
1008
1196
  var args = arguments;
1009
1197
  _.each( models, function( model ) {
1010
- model.trigger( 'destroy', model, model.collection, options );
1011
- options.error && options.error.apply( model, args );
1012
- });
1198
+ model.trigger( 'destroy', model, model.collection, options );
1199
+ options.error && options.error.apply( model, args );
1200
+ });
1013
1201
  },
1014
1202
  url: setUrl
1015
1203
  },
@@ -1043,7 +1231,7 @@
1043
1231
 
1044
1232
  // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
1045
1233
  var attributes;
1046
- if (_.isObject( key ) || key == null) {
1234
+ if ( _.isObject( key ) || key == null ) {
1047
1235
  attributes = key;
1048
1236
  options = value;
1049
1237
  }
@@ -1054,10 +1242,12 @@
1054
1242
 
1055
1243
  var result = Backbone.Model.prototype.set.apply( this, arguments );
1056
1244
 
1057
- // 'set' is called quite late in 'Backbone.Model.prototype.constructor', but before 'initialize'.
1058
1245
  // Ideal place to set up relations :)
1059
1246
  if ( !this._isInitialized && !this.isLocked() ) {
1247
+ this.constructor.initializeModelHierarchy();
1248
+
1060
1249
  Backbone.Relational.store.register( this );
1250
+
1061
1251
  this.initializeRelations();
1062
1252
  }
1063
1253
  // Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
@@ -1104,9 +1294,9 @@
1104
1294
  * and 'previousAttributes' will be available when the event is fired.
1105
1295
  */
1106
1296
  change: function( options ) {
1107
- var dit = this;
1297
+ var dit = this, args = arguments;
1108
1298
  Backbone.Relational.eventQueue.add( function() {
1109
- Backbone.Model.prototype.change.apply( dit, arguments );
1299
+ Backbone.Model.prototype.change.apply( dit, args );
1110
1300
  });
1111
1301
  },
1112
1302
 
@@ -1117,8 +1307,8 @@
1117
1307
  }
1118
1308
 
1119
1309
  _.each( this.getRelations(), function( rel ) {
1120
- delete attributes[ rel.key ];
1121
- });
1310
+ delete attributes[ rel.key ];
1311
+ });
1122
1312
 
1123
1313
  return new this.constructor( attributes );
1124
1314
  },
@@ -1135,61 +1325,275 @@
1135
1325
  this.acquire();
1136
1326
  var json = Backbone.Model.prototype.toJSON.call( this );
1137
1327
 
1328
+ if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) {
1329
+ json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue;
1330
+ }
1331
+
1138
1332
  _.each( this._relations, function( rel ) {
1139
1333
  var value = json[ rel.key ];
1140
-
1141
- if ( rel.options.includeInJSON === true && value && _.isFunction( value.toJSON ) ) {
1142
- json[ rel.key ] = value.toJSON();
1334
+
1335
+ if ( rel.options.includeInJSON === true) {
1336
+ if ( value && _.isFunction( value.toJSON ) ) {
1337
+ json[ rel.keyDestination ] = value.toJSON();
1338
+ }
1339
+ else {
1340
+ json[ rel.keyDestination ] = null;
1341
+ }
1143
1342
  }
1144
1343
  else if ( _.isString( rel.options.includeInJSON ) ) {
1145
1344
  if ( value instanceof Backbone.Collection ) {
1146
- json[ rel.key ] = value.pluck( rel.options.includeInJSON );
1345
+ json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON );
1147
1346
  }
1148
1347
  else if ( value instanceof Backbone.Model ) {
1149
- json[ rel.key ] = value.get( rel.options.includeInJSON );
1348
+ json[ rel.keyDestination ] = value.get( rel.options.includeInJSON );
1349
+ }
1350
+ else {
1351
+ json[ rel.keyDestination ] = null;
1352
+ }
1353
+ }
1354
+ else if ( _.isArray( rel.options.includeInJSON ) ) {
1355
+ if ( value instanceof Backbone.Collection ) {
1356
+ var valueSub = [];
1357
+ value.each( function( model ) {
1358
+ var curJson = {};
1359
+ _.each( rel.options.includeInJSON, function( key ) {
1360
+ curJson[ key ] = model.get( key );
1361
+ });
1362
+ valueSub.push( curJson );
1363
+ });
1364
+ json[ rel.keyDestination ] = valueSub;
1365
+ }
1366
+ else if ( value instanceof Backbone.Model ) {
1367
+ var valueSub = {};
1368
+ _.each( rel.options.includeInJSON, function( key ) {
1369
+ valueSub[ key ] = value.get( key );
1370
+ });
1371
+ json[ rel.keyDestination ] = valueSub;
1372
+ }
1373
+ else {
1374
+ json[ rel.keyDestination ] = null;
1150
1375
  }
1151
1376
  }
1152
1377
  else {
1153
1378
  delete json[ rel.key ];
1154
1379
  }
1155
- }, this );
1380
+
1381
+ if ( rel.keyDestination !== rel.key ) {
1382
+ delete json[ rel.key ];
1383
+ }
1384
+ });
1156
1385
 
1157
1386
  this.release();
1158
1387
  return json;
1159
1388
  }
1389
+ },
1390
+ {
1391
+ setup: function( superModel ) {
1392
+ // We don't want to share a relations array with a parent, as this will cause problems with
1393
+ // reverse relations.
1394
+ this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 );
1395
+
1396
+ this._subModels = {};
1397
+ this._superModel = null;
1398
+
1399
+ // If this model has 'subModelTypes' itself, remember them in the store
1400
+ if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) {
1401
+ Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this );
1402
+ }
1403
+ // The 'subModelTypes' property should not be inherited, so reset it.
1404
+ else {
1405
+ this.prototype.subModelTypes = null;
1406
+ }
1407
+
1408
+ // Initialize all reverseRelations that belong to this new model.
1409
+ _.each( this.prototype.relations, function( rel ) {
1410
+ if ( !rel.model ) {
1411
+ rel.model = this;
1412
+ }
1413
+
1414
+ if ( rel.reverseRelation && rel.model === this ) {
1415
+ var preInitialize = true;
1416
+ if ( _.isString( rel.relatedModel ) ) {
1417
+ /**
1418
+ * The related model might not be defined for two reasons
1419
+ * 1. it never gets defined, e.g. a typo
1420
+ * 2. it is related to itself
1421
+ * In neither of these cases do we need to pre-initialize reverse relations.
1422
+ */
1423
+ var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
1424
+ preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
1425
+ }
1426
+
1427
+ var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
1428
+ if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) {
1429
+ new type( null, rel );
1430
+ }
1431
+ }
1432
+ }, this );
1433
+
1434
+ return this;
1435
+ },
1436
+
1437
+ /**
1438
+ * Create a 'Backbone.Model' instance based on 'attributes'.
1439
+ * @param {Object} attributes
1440
+ * @param {Object} [options]
1441
+ * @return {Backbone.Model}
1442
+ */
1443
+ build: function( attributes, options ) {
1444
+ var model = this;
1445
+
1446
+ // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
1447
+ this.initializeModelHierarchy();
1448
+
1449
+ // Determine what type of (sub)model should be built if applicable.
1450
+ // Lookup the proper subModelType in 'this._subModels'.
1451
+ if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) {
1452
+ var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ];
1453
+ var subModelType = this._subModels[ subModelTypeAttribute ];
1454
+ if ( subModelType ) {
1455
+ model = subModelType;
1456
+ }
1457
+ }
1458
+
1459
+ return new model( attributes, options );
1460
+ },
1461
+
1462
+ initializeModelHierarchy: function() {
1463
+ // If we're here for the first time, try to determine if this modelType has a 'superModel'.
1464
+ if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) {
1465
+ Backbone.Relational.store.setupSuperModel( this );
1466
+
1467
+ // If a superModel has been found, copy relations from the _superModel if they haven't been
1468
+ // inherited automatically (due to a redefinition of 'relations').
1469
+ // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail
1470
+ // the isUndefined/isNull check next time.
1471
+ if ( this._superModel ) {
1472
+ //
1473
+ if ( this._superModel.prototype.relations ) {
1474
+ var supermodelRelationsExist = _.any( this.prototype.relations, function( rel ) {
1475
+ return rel.model && rel.model !== this;
1476
+ }, this );
1477
+
1478
+ if ( !supermodelRelationsExist ) {
1479
+ this.prototype.relations = this._superModel.prototype.relations.concat( this.prototype.relations );
1480
+ }
1481
+ }
1482
+ }
1483
+ else {
1484
+ this._superModel = false;
1485
+ }
1486
+ }
1487
+
1488
+ // If we came here through 'build' for a model that has 'subModelTypes', and not all of them have been resolved yet, try to resolve each.
1489
+ if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) {
1490
+ _.each( this.prototype.subModelTypes, function( subModelTypeName ) {
1491
+ var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
1492
+ subModelType && subModelType.initializeModelHierarchy();
1493
+ });
1494
+ }
1495
+ },
1496
+
1497
+ /**
1498
+ * Find an instance of `this` type in 'Backbone.Relational.store'.
1499
+ * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
1500
+ * - If `attributes` is an object, the model will be updated with `attributes` if found.
1501
+ * Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).
1502
+ * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
1503
+ * @param {Object} [options]
1504
+ * @param {Boolean} [options.create=true]
1505
+ * @return {Backbone.RelationalModel}
1506
+ */
1507
+ findOrCreate: function( attributes, options ) {
1508
+ // Try to find an instance of 'this' model type in the store
1509
+ var model = Backbone.Relational.store.find( this, attributes );
1510
+
1511
+ // If we found an instance, update it with the data in 'item'; if not, create an instance
1512
+ // (unless 'options.create' is false).
1513
+ if ( _.isObject( attributes ) ) {
1514
+ if ( model ) {
1515
+ model.set( attributes, options );
1516
+ }
1517
+ else if ( !options || ( options && options.create !== false ) ) {
1518
+ model = this.build( attributes, options );
1519
+ }
1520
+ }
1521
+
1522
+ return model;
1523
+ }
1160
1524
  });
1161
1525
  _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
1162
1526
 
1527
+ /**
1528
+ * Override Backbone.Collection._prepareModel, so objects will be built using the correct type
1529
+ * if the collection.model has subModels.
1530
+ */
1531
+ Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;
1532
+ Backbone.Collection.prototype._prepareModel = function ( model, options ) {
1533
+ options || (options = {});
1534
+ if ( !( model instanceof Backbone.Model ) ) {
1535
+ var attrs = model;
1536
+ options.collection = this;
1537
+
1538
+ if ( typeof this.model.build !== 'undefined' ) {
1539
+ model = this.model.build( attrs, options );
1540
+ }
1541
+ else {
1542
+ model = new this.model( attrs, options );
1543
+ }
1544
+
1545
+ if ( !model._validate( model.attributes, options ) ) {
1546
+ model = false;
1547
+ }
1548
+ }
1549
+ else if ( !model.collection ) {
1550
+ model.collection = this;
1551
+ }
1552
+
1553
+ return model;
1554
+ }
1555
+
1163
1556
  /**
1164
1557
  * Override Backbone.Collection.add, so objects fetched from the server multiple times will
1165
1558
  * update the existing Model. Also, trigger 'relational:add'.
1166
1559
  */
1167
- var add = Backbone.Collection.prototype.add;
1560
+ var add = Backbone.Collection.prototype.__add = Backbone.Collection.prototype.add;
1168
1561
  Backbone.Collection.prototype.add = function( models, options ) {
1169
1562
  options || (options = {});
1170
- if (!_.isArray( models ) ) {
1563
+ if ( !_.isArray( models ) ) {
1171
1564
  models = [ models ];
1172
1565
  }
1173
1566
 
1567
+ var modelsToAdd = [];
1568
+
1174
1569
  //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
1175
1570
  _.each( models, function( model ) {
1176
- if ( !( model instanceof Backbone.Model ) ) {
1177
- // Try to find 'model' in Backbone.store. If it already exists, set the new properties on it.
1178
- var existingModel = Backbone.Relational.store.find( this.model, model[ this.model.prototype.idAttribute ] );
1179
- if ( existingModel ) {
1180
- existingModel.set( model, options );
1181
- model = existingModel;
1571
+ if ( !( model instanceof Backbone.Model ) ) {
1572
+ // Try to find 'model' in Backbone.store. If it already exists, set the new properties on it.
1573
+ var existingModel = Backbone.Relational.store.find( this.model, model[ this.model.prototype.idAttribute ] );
1574
+ if ( existingModel ) {
1575
+ existingModel.set( existingModel.parse ? existingModel.parse( model ) : model, options );
1576
+ model = existingModel;
1577
+ }
1578
+ else {
1579
+ model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
1580
+ }
1182
1581
  }
1183
- else {
1184
- model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
1582
+
1583
+ if ( model instanceof Backbone.Model && !this.get( model ) && !this.getByCid( model ) ) {
1584
+ modelsToAdd.push( model );
1185
1585
  }
1186
- }
1586
+ }, this );
1187
1587
 
1188
- if ( model instanceof Backbone.Model && !this.get( model ) && !this.getByCid( model ) ) {
1189
- add.call( this, model, options );
1190
- this.trigger('relational:add', model, this, options);
1191
- }
1192
- }, this );
1588
+
1589
+ // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
1590
+ if ( modelsToAdd.length ) {
1591
+ add.call( this, modelsToAdd, options );
1592
+
1593
+ _.each( modelsToAdd, function( model ) {
1594
+ this.trigger( 'relational:add', model, this, options );
1595
+ }, this );
1596
+ }
1193
1597
 
1194
1598
  return this;
1195
1599
  };
@@ -1197,22 +1601,25 @@
1197
1601
  /**
1198
1602
  * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
1199
1603
  */
1200
- var remove = Backbone.Collection.prototype.remove;
1604
+ var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove;
1201
1605
  Backbone.Collection.prototype.remove = function( models, options ) {
1202
1606
  options || (options = {});
1203
- if (!_.isArray( models ) ) {
1607
+ if ( !_.isArray( models ) ) {
1204
1608
  models = [ models ];
1205
1609
  }
1610
+ else {
1611
+ models = models.slice( 0 );
1612
+ }
1206
1613
 
1207
1614
  //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
1208
1615
  _.each( models, function( model ) {
1209
- model = this.getByCid( model ) || this.get( model );
1616
+ model = this.getByCid( model ) || this.get( model );
1210
1617
 
1211
- if ( model instanceof Backbone.Model ) {
1212
- remove.call( this, model, options );
1213
- this.trigger('relational:remove', model, this, options);
1214
- }
1215
- }, this );
1618
+ if ( model instanceof Backbone.Model ) {
1619
+ remove.call( this, model, options );
1620
+ this.trigger('relational:remove', model, this, options);
1621
+ }
1622
+ }, this );
1216
1623
 
1217
1624
  return this;
1218
1625
  };
@@ -1220,10 +1627,21 @@
1220
1627
  /**
1221
1628
  * Override 'Backbone.Collection.reset' to trigger 'relational:reset'.
1222
1629
  */
1223
- var reset = Backbone.Collection.prototype.reset;
1630
+ var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset;
1224
1631
  Backbone.Collection.prototype.reset = function( models, options ) {
1225
1632
  reset.call( this, models, options );
1226
- this.trigger( 'relational:reset', models, options );
1633
+ this.trigger( 'relational:reset', this, options );
1634
+
1635
+ return this;
1636
+ };
1637
+
1638
+ /**
1639
+ * Override 'Backbone.Collection.sort' to trigger 'relational:reset'.
1640
+ */
1641
+ var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort;
1642
+ Backbone.Collection.prototype.sort = function( options ) {
1643
+ sort.call( this, options );
1644
+ this.trigger( 'relational:reset', this, options );
1227
1645
 
1228
1646
  return this;
1229
1647
  };
@@ -1232,50 +1650,37 @@
1232
1650
  * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
1233
1651
  * are ready.
1234
1652
  */
1235
- var _trigger = Backbone.Collection.prototype.trigger;
1653
+ var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger;
1236
1654
  Backbone.Collection.prototype.trigger = function( eventName ) {
1237
1655
  if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) {
1238
1656
  var dit = this, args = arguments;
1657
+
1658
+ if (eventName === 'add') {
1659
+ args = _.toArray(args);
1660
+ // the fourth argument in case of a regular add is the option object.
1661
+ // we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
1662
+ if (_.isObject(args[3])) {
1663
+ args[3] = _.clone(args[3]);
1664
+ }
1665
+ }
1666
+
1239
1667
  Backbone.Relational.eventQueue.add( function() {
1240
- _trigger.apply( dit, args );
1668
+ trigger.apply( dit, args );
1241
1669
  });
1242
1670
  }
1243
1671
  else {
1244
- _trigger.apply( this, arguments );
1672
+ trigger.apply( this, arguments );
1245
1673
  }
1246
1674
 
1247
1675
  return this;
1248
1676
  };
1249
1677
 
1250
- // Override .extend() to check for reverseRelations to initialize.
1678
+ // Override .extend() to automatically call .setup()
1251
1679
  Backbone.RelationalModel.extend = function( protoProps, classProps ) {
1252
1680
  var child = Backbone.Model.extend.apply( this, arguments );
1253
-
1254
- var relations = ( protoProps && protoProps.relations ) || [];
1255
- _.each( relations, function( rel ) {
1256
- if( rel.reverseRelation ) {
1257
- rel.model = child;
1258
-
1259
- var preInitialize = true;
1260
- if ( _.isString( rel.relatedModel ) ) {
1261
- /**
1262
- * The related model might not be defined for two reasons
1263
- * 1. it never gets defined, e.g. a typo
1264
- * 2. it is related to itself
1265
- * In neither of these cases do we need to pre-initialize reverse relations.
1266
- */
1267
- var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
1268
- preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel.prototype.constructor );
1269
- }
1270
-
1271
- var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
1272
- if ( preInitialize && type && type.prototype instanceof Backbone.Relation.prototype.constructor ) {
1273
- new type( null, rel );
1274
- }
1275
- }
1276
- });
1681
+
1682
+ child.setup( this );
1277
1683
 
1278
1684
  return child;
1279
1685
  };
1280
-
1281
1686
  })();