backbone-relational-rails 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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
  })();