backbone-relational-rails 0.7.1 → 0.8.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.7.1 == Backbone-relational 0.7.1
21
+ backbone-relational-rails 0.8.0 == Backbone-relational 0.8.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.7.1"
4
+ VERSION = "0.8.0"
5
5
  end
6
6
  end
7
7
  end
@@ -1,6 +1,6 @@
1
1
  /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
2
2
  /**
3
- * Backbone-relational.js 0.7.1
3
+ * Backbone-relational.js 0.8.0
4
4
  * (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors)
5
5
  *
6
6
  * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.
@@ -9,7 +9,7 @@
9
9
  */
10
10
  ( function( undefined ) {
11
11
  "use strict";
12
-
12
+
13
13
  /**
14
14
  * CommonJS shim
15
15
  **/
@@ -35,7 +35,7 @@
35
35
  Backbone.Semaphore = {
36
36
  _permitsAvailable: null,
37
37
  _permitsUsed: 0,
38
-
38
+
39
39
  acquire: function() {
40
40
  if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {
41
41
  throw new Error( 'Max permits acquired' );
@@ -44,7 +44,7 @@
44
44
  this._permitsUsed++;
45
45
  }
46
46
  },
47
-
47
+
48
48
  release: function() {
49
49
  if ( this._permitsUsed === 0 ) {
50
50
  throw new Error( 'All permits released' );
@@ -53,11 +53,11 @@
53
53
  this._permitsUsed--;
54
54
  }
55
55
  },
56
-
56
+
57
57
  isLocked: function() {
58
58
  return this._permitsUsed > 0;
59
59
  },
60
-
60
+
61
61
  setAvailablePermits: function( amount ) {
62
62
  if ( this._permitsUsed > amount ) {
63
63
  throw new Error( 'Available permits cannot be less than used permits' );
@@ -65,7 +65,7 @@
65
65
  this._permitsAvailable = amount;
66
66
  }
67
67
  };
68
-
68
+
69
69
  /**
70
70
  * A BlockingQueue that accumulates items while blocked (via 'block'),
71
71
  * and processes them when unblocked (via 'unblock').
@@ -76,7 +76,7 @@
76
76
  };
77
77
  _.extend( Backbone.BlockingQueue.prototype, Backbone.Semaphore, {
78
78
  _queue: null,
79
-
79
+
80
80
  add: function( func ) {
81
81
  if ( this.isBlocked() ) {
82
82
  this._queue.push( func );
@@ -85,34 +85,34 @@
85
85
  func();
86
86
  }
87
87
  },
88
-
88
+
89
89
  process: function() {
90
90
  while ( this._queue && this._queue.length ) {
91
91
  this._queue.shift()();
92
92
  }
93
93
  },
94
-
94
+
95
95
  block: function() {
96
96
  this.acquire();
97
97
  },
98
-
98
+
99
99
  unblock: function() {
100
100
  this.release();
101
101
  if ( !this.isBlocked() ) {
102
102
  this.process();
103
103
  }
104
104
  },
105
-
105
+
106
106
  isBlocked: function() {
107
107
  return this.isLocked();
108
108
  }
109
109
  });
110
110
  /**
111
- * Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'update:<key>')
111
+ * Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'change:<key>')
112
112
  * until the top-level object is fully initialized (see 'Backbone.RelationalModel').
113
113
  */
114
114
  Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
115
-
115
+
116
116
  /**
117
117
  * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
118
118
  * Handles lookup for relations.
@@ -120,10 +120,31 @@
120
120
  Backbone.Store = function() {
121
121
  this._collections = [];
122
122
  this._reverseRelations = [];
123
+ this._orphanRelations = [];
123
124
  this._subModels = [];
124
125
  this._modelScopes = [ exports ];
125
126
  };
126
127
  _.extend( Backbone.Store.prototype, Backbone.Events, {
128
+ /**
129
+ * Create a new `Relation`.
130
+ * @param {Backbone.RelationalModel} [model]
131
+ * @param {Object} relation
132
+ * @param {Object} [options]
133
+ */
134
+ initializeRelation: function( model, relation, options ) {
135
+ var type = !_.isString( relation.type ) ? relation.type : Backbone[ relation.type ] || this.getObjectByName( relation.type );
136
+ if ( type && type.prototype instanceof Backbone.Relation ) {
137
+ new type( model, relation, options ); // Also pushes the new Relation into `model._relations`
138
+ }
139
+ else {
140
+ Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid relation type!', relation );
141
+ }
142
+ },
143
+
144
+ /**
145
+ * Add a scope for `getObjectByName` to look for model types by name.
146
+ * @param {Object} scope
147
+ */
127
148
  addModelScope: function( scope ) {
128
149
  this._modelScopes.push( scope );
129
150
  },
@@ -149,7 +170,7 @@
149
170
  * @param {Backbone.RelationalModel} modelType
150
171
  */
151
172
  setupSuperModel: function( modelType ) {
152
- _.find( this._subModels || [], function( subModelDef ) {
173
+ _.find( this._subModels, function( subModelDef ) {
153
174
  return _.find( subModelDef.subModels || [], function( subModelTypeName, typeValue ) {
154
175
  var subModelType = this.getObjectByName( subModelTypeName );
155
176
 
@@ -166,7 +187,7 @@
166
187
  }, this );
167
188
  }, this );
168
189
  },
169
-
190
+
170
191
  /**
171
192
  * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
172
193
  * existing instances of 'model' in the store as well.
@@ -177,75 +198,109 @@
177
198
  * @param {String|Object} relation.relatedModel
178
199
  */
179
200
  addReverseRelation: function( relation ) {
180
- var exists = _.any( this._reverseRelations || [], function( rel ) {
181
- return _.all( relation || [], function( val, key ) {
182
- return val === rel[ key ];
183
- });
201
+ var exists = _.any( this._reverseRelations, function( rel ) {
202
+ return _.all( relation || [], function( val, key ) {
203
+ return val === rel[ key ];
184
204
  });
205
+ });
185
206
 
186
207
  if ( !exists && relation.model && relation.type ) {
187
208
  this._reverseRelations.push( relation );
188
-
189
- var addRelation = function( model, relation ) {
190
- if ( !model.prototype.relations ) {
191
- model.prototype.relations = [];
192
- }
193
- model.prototype.relations.push( relation );
194
-
195
- _.each( model._subModels || [], function( subModel ) {
196
- addRelation( subModel, relation );
197
- }, this );
198
- };
199
-
200
- addRelation( relation.model, relation );
201
-
209
+ this._addRelation( relation.model, relation );
202
210
  this.retroFitRelation( relation );
203
211
  }
204
212
  },
205
-
213
+
214
+ /**
215
+ * Deposit a `relation` for which the `relatedModel` can't be resolved at the moment.
216
+ *
217
+ * @param {Object} relation
218
+ */
219
+ addOrphanRelation: function( relation ) {
220
+ var exists = _.any( this._orphanRelations, function( rel ) {
221
+ return _.all( relation || [], function( val, key ) {
222
+ return val === rel[ key ];
223
+ });
224
+ });
225
+
226
+ if ( !exists && relation.model && relation.type ) {
227
+ this._orphanRelations.push( relation );
228
+ }
229
+ },
230
+
231
+ /**
232
+ * Try to initialize any `_orphanRelation`s
233
+ */
234
+ processOrphanRelations: function() {
235
+ // Make sure to operate on a copy since we're removing while iterating
236
+ _.each( this._orphanRelations.slice( 0 ), function( rel ) {
237
+ var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
238
+ if ( relatedModel ) {
239
+ this.initializeRelation( null, rel );
240
+ this._orphanRelations = _.without( this._orphanRelations, rel );
241
+ }
242
+ }, this );
243
+ },
244
+
245
+ /**
246
+ *
247
+ * @param {Backbone.RelationalModel.constructor} type
248
+ * @param {Object} relation
249
+ * @private
250
+ */
251
+ _addRelation: function( type, relation ) {
252
+ if ( !type.prototype.relations ) {
253
+ type.prototype.relations = [];
254
+ }
255
+ type.prototype.relations.push( relation );
256
+
257
+ _.each( type._subModels || [], function( subModel ) {
258
+ this._addRelation( subModel, relation );
259
+ }, this );
260
+ },
261
+
206
262
  /**
207
263
  * Add a 'relation' to all existing instances of 'relation.model' in the store
208
264
  * @param {Object} relation
209
265
  */
210
266
  retroFitRelation: function( relation ) {
211
- var coll = this.getCollection( relation.model );
212
- coll.each( function( model ) {
267
+ var coll = this.getCollection( relation.model, false );
268
+ coll && coll.each( function( model ) {
213
269
  if ( !( model instanceof relation.model ) ) {
214
270
  return;
215
271
  }
216
272
 
217
273
  new relation.type( model, relation );
218
- }, this);
274
+ }, this );
219
275
  },
220
-
276
+
221
277
  /**
222
278
  * Find the Store's collection for a certain type of model.
223
- * @param {Backbone.RelationalModel} model
279
+ * @param {Backbone.RelationalModel} type
280
+ * @param {Boolean} [create=true] Should a collection be created if none is found?
224
281
  * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
225
282
  */
226
- getCollection: function( model ) {
227
- if ( model instanceof Backbone.RelationalModel ) {
228
- model = model.constructor;
283
+ getCollection: function( type, create ) {
284
+ if ( type instanceof Backbone.RelationalModel ) {
285
+ type = type.constructor;
229
286
  }
230
287
 
231
- var rootModel = model;
288
+ var rootModel = type;
232
289
  while ( rootModel._superModel ) {
233
290
  rootModel = rootModel._superModel;
234
291
  }
235
292
 
236
- var coll = _.detect( this._collections, function( c ) {
237
- return c.model === rootModel;
238
- });
293
+ var coll = _.findWhere( this._collections, { model: rootModel } );
239
294
 
240
- if ( !coll ) {
295
+ if ( !coll && create !== false ) {
241
296
  coll = this._createCollection( rootModel );
242
297
  }
243
298
 
244
299
  return coll;
245
300
  },
246
-
301
+
247
302
  /**
248
- * Find a type on the global object by name. Splits name on dots.
303
+ * Find a model type on one of the modelScopes by name. Names are split on dots.
249
304
  * @param {String} name
250
305
  * @return {Object}
251
306
  */
@@ -253,7 +308,7 @@
253
308
  var parts = name.split( '.' ),
254
309
  type = null;
255
310
 
256
- _.find( this._modelScopes || [], function( scope ) {
311
+ _.find( this._modelScopes, function( scope ) {
257
312
  type = _.reduce( parts || [], function( memo, val ) {
258
313
  return memo ? memo[ val ] : undefined;
259
314
  }, scope );
@@ -265,7 +320,7 @@
265
320
 
266
321
  return type;
267
322
  },
268
-
323
+
269
324
  _createCollection: function( type ) {
270
325
  var coll;
271
326
 
@@ -332,9 +387,9 @@
332
387
 
333
388
  return null;
334
389
  },
335
-
390
+
336
391
  /**
337
- * Add a 'model' to it's appropriate collection. Retain the original contents of 'model.collection'.
392
+ * Add a 'model' to its appropriate collection. Retain the original contents of 'model.collection'.
338
393
  * @param {Backbone.RelationalModel} model
339
394
  */
340
395
  register: function( model ) {
@@ -342,42 +397,57 @@
342
397
 
343
398
  if ( coll ) {
344
399
  if ( coll.get( model ) ) {
400
+ if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
401
+ console.warn( 'Duplicate id! Old RelationalModel=%o, new RelationalModel=%o', coll.get( model ), model );
402
+ }
345
403
  throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" );
346
404
  }
347
405
 
348
406
  var modelColl = model.collection;
349
407
  coll.add( model );
350
- model.bind( 'destroy', this.unregister, this );
408
+ this.listenTo( model, 'destroy', this.unregister, this );
351
409
  model.collection = modelColl;
352
410
  }
353
411
  },
354
-
412
+
355
413
  /**
356
- * Explicitly update a model's id in it's store collection
414
+ * Explicitly update a model's id in its store collection
357
415
  * @param {Backbone.RelationalModel} model
358
- */
416
+ */
359
417
  update: function( model ) {
360
418
  var coll = this.getCollection( model );
361
419
  coll._onModelEvent( 'change:' + model.idAttribute, model, coll );
362
420
  },
363
-
421
+
364
422
  /**
365
423
  * Remove a 'model' from the store.
366
424
  * @param {Backbone.RelationalModel} model
367
425
  */
368
426
  unregister: function( model ) {
369
- model.unbind( 'destroy', this.unregister );
427
+ this.stopListening( model, 'destroy', this.unregister );
370
428
  var coll = this.getCollection( model );
371
429
  coll && coll.remove( model );
430
+ },
431
+
432
+ /**
433
+ * Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to
434
+ * re-initialize these on models would lead to a large amount of warnings.
435
+ */
436
+ reset: function() {
437
+ this.stopListening();
438
+ this._collections = [];
439
+ this._subModels = [];
440
+ this._modelScopes = [ exports ];
372
441
  }
373
442
  });
374
443
  Backbone.Relational.store = new Backbone.Store();
375
-
444
+
376
445
  /**
377
446
  * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events
378
447
  * are used to regulate addition and removal of models from relations.
379
448
  *
380
- * @param {Backbone.RelationalModel} instance
449
+ * @param {Backbone.RelationalModel} [instance] Model that this relation is created for. If no model is supplied,
450
+ * Relation just tries to instantiate it's `reverseRelation` if specified, and bails out after that.
381
451
  * @param {Object} options
382
452
  * @param {string} options.key
383
453
  * @param {Backbone.RelationalModel.constructor} options.relatedModel
@@ -386,22 +456,23 @@
386
456
  * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
387
457
  * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
388
458
  * {Backbone.Relation|String} type ('HasOne' or 'HasMany').
459
+ * @param {Object} opts
389
460
  */
390
- Backbone.Relation = function( instance, options ) {
461
+ Backbone.Relation = function( instance, options, opts ) {
391
462
  this.instance = instance;
392
463
  // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
393
464
  options = _.isObject( options ) ? options : {};
394
465
  this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
466
+ this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
467
+
395
468
  this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
396
469
  Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
397
- this.model = options.model || this.instance.constructor;
398
- this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
399
-
470
+
400
471
  this.key = this.options.key;
401
472
  this.keySource = this.options.keySource || this.key;
402
473
  this.keyDestination = this.options.keyDestination || this.keySource || this.key;
403
474
 
404
- // 'exports' should be the global object where 'relatedModel' can be found on if given as a string.
475
+ this.model = this.options.model || this.instance.constructor;
405
476
  this.relatedModel = this.options.relatedModel;
406
477
  if ( _.isString( this.relatedModel ) ) {
407
478
  this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel );
@@ -411,13 +482,26 @@
411
482
  return;
412
483
  }
413
484
 
485
+ // Add the reverse relation on 'relatedModel' to the store's reverseRelations
486
+ if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
487
+ Backbone.Relational.store.addReverseRelation( _.defaults( {
488
+ isAutoRelation: true,
489
+ model: this.relatedModel,
490
+ relatedModel: this.model,
491
+ reverseRelation: this.options // current relation is the 'reverseRelation' for its own reverseRelation
492
+ },
493
+ this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
494
+ ) );
495
+ }
496
+
414
497
  if ( instance ) {
415
498
  var contentKey = this.keySource;
416
499
  if ( contentKey !== this.key && typeof this.instance.get( this.key ) === 'object' ) {
417
500
  contentKey = this.key;
418
501
  }
419
502
 
420
- this.keyContents = this.instance.get( contentKey );
503
+ this.setKeyContents( this.instance.get( contentKey ) );
504
+ this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel );
421
505
 
422
506
  // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
423
507
  if ( this.keySource !== this.key ) {
@@ -425,38 +509,18 @@
425
509
  }
426
510
 
427
511
  // Add this Relation to instance._relations
428
- this.instance._relations.push( this );
429
- }
512
+ this.instance._relations[ this.key ] = this;
430
513
 
431
- // Add the reverse relation on 'relatedModel' to the store's reverseRelations
432
- if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
433
- Backbone.Relational.store.addReverseRelation( _.defaults( {
434
- isAutoRelation: true,
435
- model: this.relatedModel,
436
- relatedModel: this.model,
437
- reverseRelation: this.options // current relation is the 'reverseRelation' for it's own reverseRelation
438
- },
439
- this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
440
- ) );
441
- }
442
-
443
- _.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' );
444
-
445
- if ( instance ) {
446
- this.initialize();
514
+ this.initialize( opts );
447
515
 
448
- if ( options.autoFetch ) {
449
- this.instance.fetchRelated( options.key, _.isObject( options.autoFetch ) ? options.autoFetch : {} );
516
+ if ( this.options.autoFetch ) {
517
+ this.instance.fetchRelated( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} );
450
518
  }
451
519
 
452
- // When a model in the store is destroyed, check if it is 'this.instance'.
453
- Backbone.Relational.store.getCollection( this.instance )
454
- .bind( 'relational:remove', this._modelRemovedFromCollection );
455
-
456
520
  // When 'relatedModel' are created or destroyed, check if it affects this relation.
457
- Backbone.Relational.store.getCollection( this.relatedModel )
458
- .bind( 'relational:add', this._relatedModelAdded )
459
- .bind( 'relational:remove', this._relatedModelRemoved );
521
+ this.listenTo( this.instance, 'destroy', this.destroy )
522
+ .listenTo( this.relatedCollection, 'relational:add', this.tryAddRelated )
523
+ .listenTo( this.relatedCollection, 'relational:remove', this.removeRelated )
460
524
  }
461
525
  };
462
526
  // Fix inheritance :\
@@ -467,35 +531,18 @@
467
531
  createModels: true,
468
532
  includeInJSON: true,
469
533
  isAutoRelation: false,
470
- autoFetch: false
534
+ autoFetch: false,
535
+ parse: false
471
536
  },
472
-
537
+
473
538
  instance: null,
474
539
  key: null,
475
540
  keyContents: null,
476
541
  relatedModel: null,
542
+ relatedCollection: null,
477
543
  reverseRelation: null,
478
544
  related: null,
479
-
480
- _relatedModelAdded: function( model, coll, options ) {
481
- // Allow 'model' to set up it's relations, before calling 'tryAddRelated'
482
- // (which can result in a call to 'addRelated' on a relation of 'model')
483
- var dit = this;
484
- model.queue( function() {
485
- dit.tryAddRelated( model, options );
486
- });
487
- },
488
-
489
- _relatedModelRemoved: function( model, coll, options ) {
490
- this.removeRelated( model, options );
491
- },
492
-
493
- _modelRemovedFromCollection: function( model ) {
494
- if ( model === this.instance ) {
495
- this.destroy();
496
- }
497
- },
498
-
545
+
499
546
  /**
500
547
  * Check several pre-conditions.
501
548
  * @return {Boolean} True if pre-conditions are satisfied, false if they're not.
@@ -508,36 +555,33 @@
508
555
  warn = Backbone.Relational.showWarnings && typeof console !== 'undefined';
509
556
 
510
557
  if ( !m || !k || !rm ) {
511
- warn && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, m, k, rm );
558
+ warn && console.warn( 'Relation=%o: missing model, key or relatedModel (%o, %o, %o).', this, m, k, rm );
512
559
  return false;
513
560
  }
514
561
  // Check if the type in 'model' inherits from Backbone.RelationalModel
515
562
  if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
516
- warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i );
563
+ warn && console.warn( 'Relation=%o: model does not inherit from Backbone.RelationalModel (%o).', this, i );
517
564
  return false;
518
565
  }
519
566
  // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
520
567
  if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
521
- warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
568
+ warn && console.warn( 'Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).', this, rm );
522
569
  return false;
523
570
  }
524
571
  // Check if this is not a HasMany, and the reverse relation is HasMany as well
525
572
  if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
526
- warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
573
+ warn && console.warn( 'Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.', this );
527
574
  return false;
528
575
  }
576
+ // Check if we're not attempting to create a relationship on a `key` that's already used.
577
+ if ( i && _.keys( i._relations ).length ) {
578
+ var existing = _.find( i._relations, function( rel ) {
579
+ return rel.key === k;
580
+ }, this );
529
581
 
530
- // Check if we're not attempting to create a duplicate relationship
531
- if ( i && i._relations.length ) {
532
- var exists = _.any( i._relations || [], function( rel ) {
533
- var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key;
534
- return rel.relatedModel === rm && rel.key === k &&
535
- ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key );
536
- }, this );
537
-
538
- if ( exists ) {
539
- warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
540
- this, i, k, rm, this.reverseRelation.key );
582
+ if ( existing ) {
583
+ warn && console.warn( 'Cannot create relation=%o on %o for model=%o: already taken by relation=%o.',
584
+ this, k, i, existing );
541
585
  return false;
542
586
  }
543
587
  }
@@ -548,16 +592,15 @@
548
592
  /**
549
593
  * Set the related model(s) for this relation
550
594
  * @param {Backbone.Model|Backbone.Collection} related
551
- * @param {Object} [options]
552
595
  */
553
- setRelated: function( related, options ) {
596
+ setRelated: function( related ) {
554
597
  this.related = related;
555
598
 
556
599
  this.instance.acquire();
557
600
  this.instance.attributes[ this.key ] = related;
558
601
  this.instance.release();
559
602
  },
560
-
603
+
561
604
  /**
562
605
  * Determine if a relation (on a different RelationalModel) is the reverse
563
606
  * relation of the current one.
@@ -565,13 +608,10 @@
565
608
  * @return {Boolean}
566
609
  */
567
610
  _isReverseRelation: function( relation ) {
568
- if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
569
- this.key === relation.reverseRelation.key ) {
570
- return true;
571
- }
572
- return false;
611
+ return relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
612
+ this.key === relation.reverseRelation.key;
573
613
  },
574
-
614
+
575
615
  /**
576
616
  * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
577
617
  * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
@@ -583,96 +623,86 @@
583
623
  // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
584
624
  var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
585
625
  _.each( models || [], function( related ) {
586
- _.each( related.getRelations() || [], function( relation ) {
587
- if ( this._isReverseRelation( relation ) ) {
588
- reverseRelations.push( relation );
589
- }
590
- }, this );
591
- }, this );
626
+ _.each( related.getRelations() || [], function( relation ) {
627
+ if ( this._isReverseRelation( relation ) ) {
628
+ reverseRelations.push( relation );
629
+ }
630
+ }, this );
631
+ }, this );
592
632
 
593
633
  return reverseRelations;
594
634
  },
595
-
596
- /**
597
- * Rename options.silent to options.silentChange, so events propagate properly.
598
- * (for example in HasMany, from 'addRelated'->'handleAddition')
599
- * @param {Object} [options]
600
- * @return {Object}
601
- */
602
- sanitizeOptions: function( options ) {
603
- options = options ? _.clone( options ) : {};
604
- if ( options.silent ) {
605
- options.silentChange = true;
606
- delete options.silent;
607
- }
608
- return options;
609
- },
610
635
 
611
636
  /**
612
- * Rename options.silentChange to options.silent, so events are silenced as intended in Backbone's
613
- * original functions.
614
- * @param {Object} [options]
615
- * @return {Object}
637
+ * When `this.instance` is destroyed, cleanup our relations.
638
+ * Get reverse relation, call removeRelated on each.
616
639
  */
617
- unsanitizeOptions: function( options ) {
618
- options = options ? _.clone( options ) : {};
619
- if ( options.silentChange ) {
620
- options.silent = true;
621
- delete options.silentChange;
622
- }
623
- return options;
624
- },
625
-
626
- // Cleanup. Get reverse relation, call removeRelated on each.
627
640
  destroy: function() {
628
- Backbone.Relational.store.getCollection( this.instance )
629
- .unbind( 'relational:remove', this._modelRemovedFromCollection );
630
-
631
- Backbone.Relational.store.getCollection( this.relatedModel )
632
- .unbind( 'relational:add', this._relatedModelAdded )
633
- .unbind( 'relational:remove', this._relatedModelRemoved );
641
+ this.stopListening();
642
+
643
+ if ( this instanceof Backbone.HasOne ) {
644
+ this.setRelated( null );
645
+ }
646
+ else if ( this instanceof Backbone.HasMany ) {
647
+ this.setRelated( this._prepareCollection() );
648
+ }
634
649
 
635
- _.each( this.getReverseRelations() || [], function( relation ) {
636
- relation.removeRelated( this.instance );
637
- }, this );
650
+ _.each( this.getReverseRelations(), function( relation ) {
651
+ relation.removeRelated( this.instance );
652
+ }, this );
638
653
  }
639
654
  });
640
-
655
+
641
656
  Backbone.HasOne = Backbone.Relation.extend({
642
657
  options: {
643
658
  reverseRelation: { type: 'HasMany' }
644
659
  },
645
-
646
- initialize: function() {
647
- _.bindAll( this, 'onChange' );
648
660
 
649
- this.instance.bind( 'relational:change:' + this.key, this.onChange );
661
+ initialize: function( opts ) {
662
+ this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
650
663
 
651
- var model = this.findRelated( { silent: true } );
652
- this.setRelated( model );
664
+ var related = this.findRelated( opts );
665
+ this.setRelated( related );
653
666
 
654
667
  // Notify new 'related' object of the new relation.
655
- _.each( this.getReverseRelations() || [], function( relation ) {
656
- relation.addRelated( this.instance );
657
- }, this );
668
+ _.each( this.getReverseRelations(), function( relation ) {
669
+ relation.addRelated( this.instance, opts );
670
+ }, this );
658
671
  },
659
-
672
+
673
+ /**
674
+ * Find related Models.
675
+ * @param {Object} [options]
676
+ * @return {Backbone.Model}
677
+ */
660
678
  findRelated: function( options ) {
661
- var item = this.keyContents;
662
- var model = null;
663
-
664
- if ( item instanceof this.relatedModel ) {
665
- model = item;
679
+ var related = null;
680
+
681
+ options = _.defaults( { parse: this.options.parse }, options );
682
+
683
+ if ( this.keyContents instanceof this.relatedModel ) {
684
+ related = this.keyContents;
666
685
  }
667
- else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
668
- model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
686
+ else if ( this.keyContents || this.keyContents === 0 ) { // since 0 can be a valid `id` as well
687
+ var opts = _.defaults( { create: this.options.createModels }, options );
688
+ related = this.relatedModel.findOrCreate( this.keyContents, opts );
669
689
  }
670
-
671
- return model;
690
+
691
+ return related;
672
692
  },
673
-
693
+
694
+ /**
695
+ * Normalize and reduce `keyContents` to an `id`, for easier comparison
696
+ * @param {String|Number|Backbone.Model} keyContents
697
+ */
698
+ setKeyContents: function( keyContents ) {
699
+ this.keyContents = keyContents;
700
+ this.keyId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, this.keyContents );
701
+ },
702
+
674
703
  /**
675
- * If the key is changed, notify old & new reverse relations and initialize the new relation
704
+ * Event handler for `change:<key>`.
705
+ * If the key is changed, notify old & new reverse relations and initialize the new relation.
676
706
  */
677
707
  onChange: function( model, attr, options ) {
678
708
  // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
@@ -680,81 +710,70 @@
680
710
  return;
681
711
  }
682
712
  this.acquire();
683
- options = this.sanitizeOptions( options );
713
+ options = options ? _.clone( options ) : {};
684
714
 
685
- // 'options._related' is set by 'addRelated'/'removeRelated'. If it is set, the change
715
+ // 'options.__related' is set by 'addRelated'/'removeRelated'. If it is set, the change
686
716
  // is the result of a call from a relation. If it's not, the change is the result of
687
717
  // a 'set' call on this.instance.
688
- var changed = _.isUndefined( options._related );
689
- var oldRelated = changed ? this.related : options._related;
718
+ var changed = _.isUndefined( options.__related ),
719
+ oldRelated = changed ? this.related : options.__related;
690
720
 
691
- if ( changed ) {
692
- this.keyContents = attr;
693
-
694
- // Set new 'related'
695
- if ( attr instanceof this.relatedModel ) {
696
- this.related = attr;
697
- }
698
- else if ( attr ) {
699
- var related = this.findRelated( options );
700
- this.setRelated( related );
701
- }
702
- else {
703
- this.setRelated( null );
704
- }
721
+ if ( changed ) {
722
+ this.setKeyContents( attr );
723
+ var related = this.findRelated( options );
724
+ this.setRelated( related );
705
725
  }
706
726
 
707
727
  // Notify old 'related' object of the terminated relation
708
728
  if ( oldRelated && this.related !== oldRelated ) {
709
- _.each( this.getReverseRelations( oldRelated ) || [], function( relation ) {
710
- relation.removeRelated( this.instance, options );
711
- }, this );
729
+ _.each( this.getReverseRelations( oldRelated ), function( relation ) {
730
+ relation.removeRelated( this.instance, null, options );
731
+ }, this );
712
732
  }
713
-
733
+
714
734
  // Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated;
715
735
  // that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'.
716
736
  // In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet.
717
- _.each( this.getReverseRelations() || [], function( relation ) {
718
- relation.addRelated( this.instance, options );
719
- }, this);
737
+ _.each( this.getReverseRelations(), function( relation ) {
738
+ relation.addRelated( this.instance, options );
739
+ }, this );
720
740
 
721
- // Fire the 'update:<key>' event if 'related' was updated
722
- if ( !options.silentChange && this.related !== oldRelated ) {
741
+ // Fire the 'change:<key>' event if 'related' was updated
742
+ if ( !options.silent && this.related !== oldRelated ) {
723
743
  var dit = this;
744
+ this.changed = true;
724
745
  Backbone.Relational.eventQueue.add( function() {
725
- dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
746
+ dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
747
+ dit.changed = false;
726
748
  });
727
749
  }
728
750
  this.release();
729
751
  },
730
-
752
+
731
753
  /**
732
754
  * If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents'
733
755
  */
734
- tryAddRelated: function( model, options ) {
735
- if ( this.related ) {
736
- return;
737
- }
738
- options = this.sanitizeOptions( options );
739
-
740
- var item = this.keyContents;
741
- if ( item || item === 0 ) { // since 0 can be a valid `id` as well
742
- var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
743
- if ( !_.isNull( id ) && model.id === id ) {
744
- this.addRelated( model, options );
745
- }
756
+ tryAddRelated: function( model, coll, options ) {
757
+ if ( ( this.keyId || this.keyId === 0 ) && model.id === this.keyId ) { // since 0 can be a valid `id` as well
758
+ this.addRelated( model, options );
759
+ this.keyId = null;
746
760
  }
747
761
  },
748
-
762
+
749
763
  addRelated: function( model, options ) {
750
- if ( model !== this.related ) {
751
- var oldRelated = this.related || null;
752
- this.setRelated( model );
753
- this.onChange( this.instance, model, { _related: oldRelated } );
754
- }
764
+ // Allow 'model' to set up its relations before proceeding.
765
+ // (which can result in a call to 'addRelated' from a relation of 'model')
766
+ var dit = this;
767
+ model.queue( function() {
768
+ if ( model !== dit.related ) {
769
+ var oldRelated = dit.related || null;
770
+ dit.setRelated( model );
771
+ dit.onChange( dit.instance, model, _.defaults( { __related: oldRelated }, options ) );
772
+ }
773
+ });
755
774
  },
756
-
757
- removeRelated: function( model, options ) {
775
+
776
+ removeRelated: function( model, coll, options ) {
758
777
  if ( !this.related ) {
759
778
  return;
760
779
  }
@@ -762,24 +781,23 @@
762
781
  if ( model === this.related ) {
763
782
  var oldRelated = this.related || null;
764
783
  this.setRelated( null );
765
- this.onChange( this.instance, model, { _related: oldRelated } );
784
+ this.onChange( this.instance, model, _.defaults( { __related: oldRelated }, options ) );
766
785
  }
767
786
  }
768
787
  });
769
-
788
+
770
789
  Backbone.HasMany = Backbone.Relation.extend({
771
790
  collectionType: null,
772
-
791
+
773
792
  options: {
774
793
  reverseRelation: { type: 'HasOne' },
775
794
  collectionType: Backbone.Collection,
776
795
  collectionKey: true,
777
796
  collectionOptions: {}
778
797
  },
779
-
780
- initialize: function() {
781
- _.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval', 'handleReset' );
782
- this.instance.bind( 'relational:change:' + this.key, this.onChange );
798
+
799
+ initialize: function( opts ) {
800
+ this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
783
801
 
784
802
  // Handle a custom 'collectionType'
785
803
  this.collectionType = this.options.collectionType;
@@ -787,41 +805,29 @@
787
805
  this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
788
806
  }
789
807
  if ( !this.collectionType.prototype instanceof Backbone.Collection ){
790
- throw new Error( 'collectionType must inherit from Backbone.Collection' );
808
+ throw new Error( '`collectionType` must inherit from Backbone.Collection' );
791
809
  }
792
810
 
793
- // Handle cases where a model/relation is created with a collection passed straight into 'attributes'
794
- if ( this.keyContents instanceof Backbone.Collection ) {
795
- this.setRelated( this._prepareCollection( this.keyContents ) );
796
- }
797
- else {
798
- this.setRelated( this._prepareCollection() );
799
- }
800
-
801
- this.findRelated( { silent: true } );
802
- },
803
-
804
- _getCollectionOptions: function() {
805
- return _.isFunction( this.options.collectionOptions ) ?
806
- this.options.collectionOptions( this.instance ) :
807
- this.options.collectionOptions;
811
+ var related = this.findRelated( opts );
812
+ this.setRelated( related );
808
813
  },
809
814
 
810
815
  /**
811
816
  * Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany.
812
817
  * If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option.
813
818
  * @param {Backbone.Collection} [collection]
819
+ * @return {Backbone.Collection}
814
820
  */
815
821
  _prepareCollection: function( collection ) {
816
822
  if ( this.related ) {
817
- this.related
818
- .unbind( 'relational:add', this.handleAddition )
819
- .unbind( 'relational:remove', this.handleRemoval )
820
- .unbind( 'relational:reset', this.handleReset )
823
+ this.stopListening( this.related );
821
824
  }
822
825
 
823
826
  if ( !collection || !( collection instanceof Backbone.Collection ) ) {
824
- collection = new this.collectionType( [], this._getCollectionOptions() );
827
+ var options = _.isFunction( this.options.collectionOptions ) ?
828
+ this.options.collectionOptions( this.instance ) : this.options.collectionOptions;
829
+
830
+ collection = new this.collectionType( null, options );
825
831
  }
826
832
 
827
833
  collection.model = this.relatedModel;
@@ -838,203 +844,184 @@
838
844
  collection[ key ] = this.instance;
839
845
  }
840
846
  }
841
-
842
- collection
843
- .bind( 'relational:add', this.handleAddition )
844
- .bind( 'relational:remove', this.handleRemoval )
845
- .bind( 'relational:reset', this.handleReset );
847
+
848
+ this.listenTo( collection, 'relational:add', this.handleAddition )
849
+ .listenTo( collection, 'relational:remove', this.handleRemoval )
850
+ .listenTo( collection, 'relational:reset', this.handleReset );
846
851
 
847
852
  return collection;
848
853
  },
849
-
850
- findRelated: function( options ) {
851
- if ( this.keyContents ) {
852
- var models = [];
853
854
 
854
- if ( this.keyContents instanceof Backbone.Collection ) {
855
- models = this.keyContents.models;
856
- }
857
- else {
858
- // Handle cases the an API/user supplies just an Object/id instead of an Array
859
- this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ];
860
-
861
- // Try to find instances of the appropriate 'relatedModel' in the store
862
- _.each( this.keyContents || [], function( item ) {
863
- var model = null;
864
- if ( item instanceof this.relatedModel ) {
865
- model = item;
866
- }
867
- else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
868
- model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
869
- }
855
+ /**
856
+ * Find related Models.
857
+ * @param {Object} [options]
858
+ * @return {Backbone.Collection}
859
+ */
860
+ findRelated: function( options ) {
861
+ var related = null;
870
862
 
871
- if ( model && !this.related.get( model ) ) {
872
- models.push( model );
873
- }
874
- }, this );
875
- }
863
+ options = _.defaults( { parse: this.options.parse }, options );
876
864
 
877
- // Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.)
878
- if ( models.length ) {
879
- options = this.unsanitizeOptions( options );
880
- this.related.add( models, options );
881
- }
865
+ // Replace 'this.related' by 'this.keyContents' if it is a Backbone.Collection
866
+ if ( this.keyContents instanceof Backbone.Collection ) {
867
+ this._prepareCollection( this.keyContents );
868
+ related = this.keyContents;
882
869
  }
883
- },
884
-
885
- /**
886
- * If the key is changed, notify old & new reverse relations and initialize the new relation
887
- */
888
- onChange: function( model, attr, options ) {
889
- options = this.sanitizeOptions( options );
890
- this.keyContents = attr;
891
-
892
- // Replace 'this.related' by 'attr' if it is a Backbone.Collection
893
- if ( attr instanceof Backbone.Collection ) {
894
- this._prepareCollection( attr );
895
- this.related = attr;
896
- }
897
- // Otherwise, 'attr' should be an array of related object ids.
898
- // Re-use the current 'this.related' if it is a Backbone.Collection, and remove any current entries.
899
- // Otherwise, create a new collection.
870
+ // Otherwise, 'this.keyContents' should be an array of related object ids.
871
+ // Re-use the current 'this.related' if it is a Backbone.Collection; otherwise, create a new collection.
900
872
  else {
901
- var oldIds = {}, newIds = {};
873
+ var toAdd = [];
902
874
 
903
- if (!_.isArray( attr ) && attr !== undefined) {
904
- attr = [ attr ];
905
- }
875
+ _.each( this.keyContents, function( attributes ) {
876
+ if ( attributes instanceof this.relatedModel ) {
877
+ var model = attributes;
878
+ }
879
+ else {
880
+ // If `merge` is true, update models here, instead of during update.
881
+ model = this.relatedModel.findOrCreate( attributes, _.extend( { merge: true }, options, { create: this.options.createModels } ) );
882
+ }
906
883
 
907
- _.each( attr, function( attributes ) {
908
- newIds[ attributes.id ] = true;
909
- });
884
+ model && toAdd.push( model );
885
+ }, this );
910
886
 
911
- var coll = this.related;
912
- if ( coll instanceof Backbone.Collection ) {
913
- // Make sure to operate on a copy since we're removing while iterating
914
- _.each( coll.models.slice(0) , function( model ) {
915
- // When fetch is called with the 'keepNewModels' option, we don't want to remove
916
- // client-created new models when the fetch is completed.
917
- if ( !options.keepNewModels || !model.isNew() ) {
918
- oldIds[ model.id ] = true;
919
- coll.remove( model, { silent: (model.id in newIds) } );
920
- }
921
- });
922
- } else {
923
- coll = this._prepareCollection();
887
+ if ( this.related instanceof Backbone.Collection ) {
888
+ related = this.related;
889
+ }
890
+ else {
891
+ related = this._prepareCollection();
924
892
  }
925
893
 
926
- _.each( attr, function( attributes ) {
927
- var model = this.relatedModel.findOrCreate( attributes, { create: this.options.createModels } );
928
- if (model) {
929
- coll.add( model, { silent: (attributes.id in oldIds)} );
930
- }
931
- }, this);
894
+ related.update( toAdd, _.defaults( { merge: false, parse: false }, options ) );
895
+ }
932
896
 
933
- this.setRelated( coll );
897
+ return related;
898
+ },
934
899
 
900
+ /**
901
+ * Normalize and reduce `keyContents` to a list of `ids`, for easier comparison
902
+ * @param {String|Number|String[]|Number[]|Backbone.Collection} keyContents
903
+ */
904
+ setKeyContents: function( keyContents ) {
905
+ this.keyContents = keyContents instanceof Backbone.Collection ? keyContents : null;
906
+ this.keyIds = [];
907
+
908
+ if ( !this.keyContents && ( keyContents || keyContents === 0 ) ) { // since 0 can be a valid `id` as well
909
+ // Handle cases the an API/user supplies just an Object/id instead of an Array
910
+ this.keyContents = _.isArray( keyContents ) ? keyContents : [ keyContents ];
911
+
912
+ _.each( this.keyContents, function( item ) {
913
+ var itemId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
914
+ if ( itemId || itemId === 0 ) {
915
+ this.keyIds.push( itemId );
916
+ }
917
+ }, this );
935
918
  }
936
-
937
- var dit = this;
938
- Backbone.Relational.eventQueue.add( function() {
939
- !options.silentChange && dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
940
- });
941
919
  },
942
-
943
- tryAddRelated: function( model, options ) {
944
- options = this.sanitizeOptions( options );
945
- if ( !this.related.get( model ) ) {
946
- // Check if this new model was specified in 'this.keyContents'
947
- var item = _.any( this.keyContents || [], function( item ) {
948
- var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
949
- return !_.isNull( id ) && id === model.id;
950
- }, this );
951
-
952
- if ( item ) {
953
- this.related.add( model, options );
954
- }
920
+
921
+ /**
922
+ * Event handler for `change:<key>`.
923
+ * If the contents of the key are changed, notify old & new reverse relations and initialize the new relation.
924
+ */
925
+ onChange: function( model, attr, options ) {
926
+ options = options ? _.clone( options ) : {};
927
+ this.setKeyContents( attr );
928
+ this.changed = false;
929
+
930
+ var related = this.findRelated( options );
931
+ this.setRelated( related );
932
+
933
+ if ( !options.silent ) {
934
+ var dit = this;
935
+ Backbone.Relational.eventQueue.add( function() {
936
+ // The `changed` flag can be set in `handleAddition` or `handleRemoval`
937
+ if ( dit.changed ) {
938
+ dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
939
+ dit.changed = false;
940
+ }
941
+ });
955
942
  }
956
943
  },
957
-
944
+
958
945
  /**
959
946
  * When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
960
947
  * (should be 'HasOne', must set 'this.instance' as their related).
961
- */
948
+ */
962
949
  handleAddition: function( model, coll, options ) {
963
950
  //console.debug('handleAddition called; args=%o', arguments);
964
- // Make sure the model is in fact a valid model before continuing.
965
- // (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel)
966
- if ( !( model instanceof Backbone.Model ) ) {
967
- return;
968
- }
969
-
970
- options = this.sanitizeOptions( options );
951
+ options = options ? _.clone( options ) : {};
952
+ this.changed = true;
971
953
 
972
- _.each( this.getReverseRelations( model ) || [], function( relation ) {
973
- relation.addRelated( this.instance, options );
974
- }, this );
954
+ _.each( this.getReverseRelations( model ), function( relation ) {
955
+ relation.addRelated( this.instance, options );
956
+ }, this );
975
957
 
976
- // Only trigger 'add' once the newly added model is initialized (so, has it's relations set up)
958
+ // Only trigger 'add' once the newly added model is initialized (so, has its relations set up)
977
959
  var dit = this;
978
- Backbone.Relational.eventQueue.add( function() {
979
- !options.silentChange && dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
960
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
961
+ dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
980
962
  });
981
963
  },
982
-
964
+
983
965
  /**
984
966
  * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations.
985
967
  * (should be 'HasOne', which should be nullified)
986
968
  */
987
969
  handleRemoval: function( model, coll, options ) {
988
970
  //console.debug('handleRemoval called; args=%o', arguments);
989
- if ( !( model instanceof Backbone.Model ) ) {
990
- return;
991
- }
992
-
993
- options = this.sanitizeOptions( options );
971
+ options = options ? _.clone( options ) : {};
972
+ this.changed = true;
994
973
 
995
- _.each( this.getReverseRelations( model ) || [], function( relation ) {
996
- relation.removeRelated( this.instance, options );
997
- }, this );
974
+ _.each( this.getReverseRelations( model ), function( relation ) {
975
+ relation.removeRelated( this.instance, null, options );
976
+ }, this );
998
977
 
999
978
  var dit = this;
1000
- Backbone.Relational.eventQueue.add( function() {
1001
- !options.silentChange && dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
979
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
980
+ dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
1002
981
  });
1003
982
  },
1004
983
 
1005
984
  handleReset: function( coll, options ) {
1006
- options = this.sanitizeOptions( options );
1007
-
1008
985
  var dit = this;
1009
- Backbone.Relational.eventQueue.add( function() {
1010
- !options.silentChange && dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
986
+ options = options ? _.clone( options ) : {};
987
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
988
+ dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
1011
989
  });
1012
990
  },
1013
-
991
+
992
+ tryAddRelated: function( model, coll, options ) {
993
+ var item = _.contains( this.keyIds, model.id );
994
+
995
+ if ( item ) {
996
+ this.addRelated( model, options );
997
+ this.keyIds = _.without( this.keyIds, model.id );
998
+ }
999
+ },
1000
+
1014
1001
  addRelated: function( model, options ) {
1002
+ // Allow 'model' to set up its relations before proceeding.
1003
+ // (which can result in a call to 'addRelated' from a relation of 'model')
1015
1004
  var dit = this;
1016
- options = this.unsanitizeOptions( options );
1017
- model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice
1005
+ model.queue( function() {
1018
1006
  if ( dit.related && !dit.related.get( model ) ) {
1019
1007
  dit.related.add( model, options );
1020
1008
  }
1021
1009
  });
1022
1010
  },
1023
-
1024
- removeRelated: function( model, options ) {
1025
- options = this.unsanitizeOptions( options );
1011
+
1012
+ removeRelated: function( model, coll, options ) {
1026
1013
  if ( this.related.get( model ) ) {
1027
1014
  this.related.remove( model, options );
1028
1015
  }
1029
1016
  }
1030
1017
  });
1031
-
1018
+
1032
1019
  /**
1033
1020
  * A type of Backbone.Model that also maintains relations to other models and collections.
1034
1021
  * New events when compared to the original:
1035
1022
  * - 'add:<key>' (model, related collection, options)
1036
1023
  * - 'remove:<key>' (model, related collection, options)
1037
- * - 'update:<key>' (model, related model or collection, options)
1024
+ * - 'change:<key>' (model, related model or collection, options)
1038
1025
  */
1039
1026
  Backbone.RelationalModel = Backbone.Model.extend({
1040
1027
  relations: null, // Relation descriptions on the prototype
@@ -1042,56 +1029,98 @@
1042
1029
  _isInitialized: false,
1043
1030
  _deferProcessing: false,
1044
1031
  _queue: null,
1045
-
1032
+
1046
1033
  subModelTypeAttribute: 'type',
1047
1034
  subModelTypes: null,
1048
-
1035
+
1049
1036
  constructor: function( attributes, options ) {
1050
1037
  // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
1051
- // Defer 'processQueue', so that when 'Relation.createModels' is used we:
1052
- // a) Survive 'Backbone.Collection.add'; this takes care we won't error on "can't add model to a set twice"
1053
- // (by creating a model from properties, having the model add itself to the collection via one of
1054
- // it's relations, then trying to add it to the collection).
1055
- // b) Trigger 'HasMany' collection events only after the model is really fully set up.
1056
- // Example that triggers both a and b: "p.get('jobs').add( { company: c, person: p } )".
1057
- var dit = this;
1038
+ // Defer 'processQueue', so that when 'Relation.createModels' is used we trigger 'HasMany'
1039
+ // collection events only after the model is really fully set up.
1040
+ // Example: "p.get('jobs').add( { company: c, person: p } )".
1058
1041
  if ( options && options.collection ) {
1042
+ var dit = this,
1043
+ collection = this.collection = options.collection;
1044
+
1045
+ // Prevent this option from cascading down to related models; they shouldn't go into this `if` clause.
1046
+ delete options.collection;
1047
+
1059
1048
  this._deferProcessing = true;
1060
-
1049
+
1061
1050
  var processQueue = function( model ) {
1062
1051
  if ( model === dit ) {
1063
1052
  dit._deferProcessing = false;
1064
1053
  dit.processQueue();
1065
- options.collection.unbind( 'relational:add', processQueue );
1054
+ collection.off( 'relational:add', processQueue );
1066
1055
  }
1067
1056
  };
1068
- options.collection.bind( 'relational:add', processQueue );
1069
-
1070
- // So we do process the queue eventually, regardless of whether this model really gets added to 'options.collection'.
1057
+ collection.on( 'relational:add', processQueue );
1058
+
1059
+ // So we do process the queue eventually, regardless of whether this model actually gets added to 'options.collection'.
1071
1060
  _.defer( function() {
1072
1061
  processQueue( dit );
1073
1062
  });
1074
1063
  }
1064
+
1065
+ Backbone.Relational.store.processOrphanRelations();
1075
1066
 
1076
1067
  this._queue = new Backbone.BlockingQueue();
1077
1068
  this._queue.block();
1078
1069
  Backbone.Relational.eventQueue.block();
1079
-
1080
- Backbone.Model.apply( this, arguments );
1081
-
1082
- // Try to run the global queue holding external events
1083
- Backbone.Relational.eventQueue.unblock();
1070
+
1071
+ try {
1072
+ Backbone.Model.apply( this, arguments );
1073
+ }
1074
+ finally {
1075
+ // Try to run the global queue holding external events
1076
+ Backbone.Relational.eventQueue.unblock();
1077
+ }
1084
1078
  },
1085
-
1079
+
1086
1080
  /**
1087
1081
  * Override 'trigger' to queue 'change' and 'change:*' events
1088
1082
  */
1089
1083
  trigger: function( eventName ) {
1090
- if ( eventName.length > 5 && 'change' === eventName.substr( 0, 6 ) ) {
1091
- var dit = this, args = arguments;
1084
+ if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) {
1085
+ var dit = this,
1086
+ args = arguments;
1087
+
1092
1088
  Backbone.Relational.eventQueue.add( function() {
1093
- Backbone.Model.prototype.trigger.apply( dit, args );
1094
- });
1089
+ if ( !dit._isInitialized ) {
1090
+ return;
1091
+ }
1092
+
1093
+ // Determine if the `change` event is still valid, now that all relations are populated
1094
+ var changed = true;
1095
+ if ( eventName === 'change' ) {
1096
+ changed = dit.hasChanged();
1097
+ }
1098
+ else {
1099
+ var attr = eventName.slice( 7 ),
1100
+ rel = dit.getRelation( attr );
1101
+
1102
+ if ( rel ) {
1103
+ // If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`.
1104
+ // These take precedence over `change:attr` events triggered by `Model.set`.
1105
+ // The relation set a fourth attribute to `true`. If this attribute is present,
1106
+ // continue triggering this event; otherwise, it's from `Model.set` and should be stopped.
1107
+ changed = ( args[ 4 ] === true );
1108
+
1109
+ // If this event was triggered by a relation, set the right value in `this.changed`
1110
+ // (a Collection or Model instead of raw data).
1111
+ if ( changed ) {
1112
+ dit.changed[ attr ] = args[ 2 ];
1113
+ }
1114
+ // Otherwise, this event is from `Model.set`. If the relation doesn't report a change,
1115
+ // remove attr from `dit.changed` so `hasChanged` doesn't take it into account.
1116
+ else if ( !rel.changed ) {
1117
+ delete dit.changed[ attr ];
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ changed && Backbone.Model.prototype.trigger.apply( dit, args );
1123
+ });
1095
1124
  }
1096
1125
  else {
1097
1126
  Backbone.Model.prototype.trigger.apply( this, arguments );
@@ -1099,24 +1128,18 @@
1099
1128
 
1100
1129
  return this;
1101
1130
  },
1102
-
1131
+
1103
1132
  /**
1104
1133
  * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance.
1105
1134
  * Invoked in the first call so 'set' (which is made from the Backbone.Model constructor).
1106
1135
  */
1107
- initializeRelations: function() {
1136
+ initializeRelations: function( options ) {
1108
1137
  this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
1109
- this._relations = [];
1138
+ this._relations = {};
1110
1139
 
1111
1140
  _.each( this.relations || [], function( rel ) {
1112
- var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
1113
- if ( type && type.prototype instanceof Backbone.Relation ) {
1114
- new type( this, rel ); // Also pushes the new Relation into _relations
1115
- }
1116
- else {
1117
- Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid type!', rel );
1118
- }
1119
- }, this );
1141
+ Backbone.Relational.store.initializeRelation( this, rel, options );
1142
+ }, this );
1120
1143
 
1121
1144
  this._isInitialized = true;
1122
1145
  this.release();
@@ -1129,7 +1152,7 @@
1129
1152
  */
1130
1153
  updateRelations: function( options ) {
1131
1154
  if ( this._isInitialized && !this.isLocked() ) {
1132
- _.each( this._relations || [], function( rel ) {
1155
+ _.each( this._relations, function( rel ) {
1133
1156
  // Update from data in `rel.keySource` if set, or `rel.key` otherwise
1134
1157
  var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ];
1135
1158
  if ( rel.related !== val ) {
@@ -1138,14 +1161,14 @@
1138
1161
  }, this );
1139
1162
  }
1140
1163
  },
1141
-
1164
+
1142
1165
  /**
1143
1166
  * Either add to the queue (if we're not initialized yet), or execute right away.
1144
1167
  */
1145
1168
  queue: function( func ) {
1146
1169
  this._queue.add( func );
1147
1170
  },
1148
-
1171
+
1149
1172
  /**
1150
1173
  * Process _queue
1151
1174
  */
@@ -1154,62 +1177,58 @@
1154
1177
  this._queue.unblock();
1155
1178
  }
1156
1179
  },
1157
-
1180
+
1158
1181
  /**
1159
1182
  * Get a specific relation.
1160
1183
  * @param key {string} The relation key to look for.
1161
1184
  * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null.
1162
1185
  */
1163
1186
  getRelation: function( key ) {
1164
- return _.detect( this._relations, function( rel ) {
1165
- if ( rel.key === key ) {
1166
- return true;
1167
- }
1168
- }, this );
1187
+ return this._relations[ key ];
1169
1188
  },
1170
-
1189
+
1171
1190
  /**
1172
1191
  * Get all of the created relations.
1173
1192
  * @return {Backbone.Relation[]}
1174
1193
  */
1175
1194
  getRelations: function() {
1176
- return this._relations;
1195
+ return _.values( this._relations );
1177
1196
  },
1178
-
1197
+
1179
1198
  /**
1180
1199
  * Retrieve related objects.
1181
1200
  * @param key {string} The relation key to fetch models for.
1182
1201
  * @param [options] {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
1183
- * @param [update=false] {boolean} Whether to force a fetch from the server (updating existing models).
1202
+ * @param [refresh=false] {boolean} Fetch existing models from the server as well (in order to update them).
1184
1203
  * @return {jQuery.when[]} An array of request objects
1185
1204
  */
1186
- fetchRelated: function( key, options, update ) {
1187
- options || ( options = {} );
1205
+ fetchRelated: function( key, options, refresh ) {
1206
+ // Set default `options` for fetch
1207
+ options = _.extend( { update: true, remove: false }, options );
1208
+
1188
1209
  var setUrl,
1189
1210
  requests = [],
1190
1211
  rel = this.getRelation( key ),
1191
- keyContents = rel && rel.keyContents,
1192
- toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) {
1193
- var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item );
1194
- return !_.isNull( id ) && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) );
1212
+ keys = rel && ( rel.keyIds || [ rel.keyId ] ),
1213
+ toFetch = keys && _.select( keys || [], function( id ) {
1214
+ return ( id || id === 0 ) && ( refresh || !Backbone.Relational.store.find( rel.relatedModel, id ) );
1195
1215
  }, this );
1196
1216
 
1197
1217
  if ( toFetch && toFetch.length ) {
1198
- // Create a model for each entry in 'keyContents' that is to be fetched
1199
- var models = _.map( toFetch, function( item ) {
1200
- var model;
1201
-
1202
- if ( _.isObject( item ) ) {
1203
- model = rel.relatedModel.findOrCreate( item );
1204
- }
1205
- else {
1206
- var attrs = {};
1207
- attrs[ rel.relatedModel.prototype.idAttribute ] = item;
1208
- model = rel.relatedModel.findOrCreate( attrs );
1209
- }
1218
+ // Find (or create) a model for each one that is to be fetched
1219
+ var created = [],
1220
+ models = _.map( toFetch, function( id ) {
1221
+ var model = Backbone.Relational.store.find( rel.relatedModel, id );
1222
+
1223
+ if ( !model ) {
1224
+ var attrs = {};
1225
+ attrs[ rel.relatedModel.prototype.idAttribute ] = id;
1226
+ model = rel.relatedModel.findOrCreate( attrs, options );
1227
+ created.push( model );
1228
+ }
1210
1229
 
1211
- return model;
1212
- }, this );
1230
+ return model;
1231
+ }, this );
1213
1232
 
1214
1233
  // Try if the 'collection' can provide a url to fetch a set of models in one request.
1215
1234
  if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) {
@@ -1224,26 +1243,27 @@
1224
1243
  {
1225
1244
  error: function() {
1226
1245
  var args = arguments;
1227
- _.each( models || [], function( model ) {
1228
- model.trigger( 'destroy', model, model.collection, options );
1229
- options.error && options.error.apply( model, args );
1230
- });
1246
+ _.each( created, function( model ) {
1247
+ model.trigger( 'destroy', model, model.collection, options );
1248
+ options.error && options.error.apply( model, args );
1249
+ });
1231
1250
  },
1232
1251
  url: setUrl
1233
1252
  },
1234
- options,
1235
- { add: true }
1253
+ options
1236
1254
  );
1237
1255
 
1238
1256
  requests = [ rel.related.fetch( opts ) ];
1239
1257
  }
1240
1258
  else {
1241
- requests = _.map( models || [], function( model ) {
1259
+ requests = _.map( models, function( model ) {
1242
1260
  var opts = _.defaults(
1243
1261
  {
1244
1262
  error: function() {
1245
- model.trigger( 'destroy', model, model.collection, options );
1246
- options.error && options.error.apply( model, arguments );
1263
+ if ( _.contains( created, model ) ) {
1264
+ model.trigger( 'destroy', model, model.collection, options );
1265
+ options.error && options.error.apply( model, arguments );
1266
+ }
1247
1267
  }
1248
1268
  },
1249
1269
  options
@@ -1255,7 +1275,32 @@
1255
1275
 
1256
1276
  return requests;
1257
1277
  },
1258
-
1278
+
1279
+ get: function( attr ) {
1280
+ var originalResult = Backbone.Model.prototype.get.call( this, attr );
1281
+
1282
+ // Use `originalResult` get if dotNotation not enabled or not required because no dot is in `attr`
1283
+ if ( !this.dotNotation || attr.indexOf( '.' ) === -1 ) {
1284
+ return originalResult;
1285
+ }
1286
+
1287
+ // Go through all splits and return the final result
1288
+ var splits = attr.split( '.' );
1289
+ var result = _.reduce(splits, function( model, split ) {
1290
+ if ( !( model instanceof Backbone.Model ) ) {
1291
+ throw new Error( 'Attribute must be an instanceof Backbone.Model. Is: ' + model + ', currentSplit: ' + split );
1292
+ }
1293
+
1294
+ return Backbone.Model.prototype.get.call( model, split );
1295
+ }, this );
1296
+
1297
+ if ( originalResult !== undefined && result !== undefined ) {
1298
+ throw new Error( "Ambiguous result for '" + attr + "'. direct result: " + originalResult + ", dotNotation: " + result );
1299
+ }
1300
+
1301
+ return originalResult || result;
1302
+ },
1303
+
1259
1304
  set: function( key, value, options ) {
1260
1305
  Backbone.Relational.eventQueue.block();
1261
1306
 
@@ -1273,28 +1318,31 @@
1273
1318
  var result = Backbone.Model.prototype.set.apply( this, arguments );
1274
1319
 
1275
1320
  // Ideal place to set up relations :)
1276
- if ( !this._isInitialized && !this.isLocked() ) {
1277
- this.constructor.initializeModelHierarchy();
1321
+ try {
1322
+ if ( !this._isInitialized && !this.isLocked() ) {
1323
+ this.constructor.initializeModelHierarchy();
1278
1324
 
1279
- Backbone.Relational.store.register( this );
1325
+ Backbone.Relational.store.register( this );
1280
1326
 
1281
- this.initializeRelations();
1282
- }
1283
- // Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
1284
- else if ( attributes && this.idAttribute in attributes ) {
1285
- Backbone.Relational.store.update( this );
1327
+ this.initializeRelations( options );
1328
+ }
1329
+ // Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
1330
+ else if ( attributes && this.idAttribute in attributes ) {
1331
+ Backbone.Relational.store.update( this );
1332
+ }
1333
+
1334
+ if ( attributes ) {
1335
+ this.updateRelations( options );
1336
+ }
1286
1337
  }
1287
-
1288
- if ( attributes ) {
1289
- this.updateRelations( options );
1338
+ finally {
1339
+ // Try to run the global queue holding external events
1340
+ Backbone.Relational.eventQueue.unblock();
1290
1341
  }
1291
1342
 
1292
- // Try to run the global queue holding external events
1293
- Backbone.Relational.eventQueue.unblock();
1294
-
1295
1343
  return result;
1296
1344
  },
1297
-
1345
+
1298
1346
  unset: function( attribute, options ) {
1299
1347
  Backbone.Relational.eventQueue.block();
1300
1348
 
@@ -1306,7 +1354,7 @@
1306
1354
 
1307
1355
  return result;
1308
1356
  },
1309
-
1357
+
1310
1358
  clear: function( options ) {
1311
1359
  Backbone.Relational.eventQueue.block();
1312
1360
 
@@ -1325,17 +1373,17 @@
1325
1373
  attributes[ this.idAttribute ] = null;
1326
1374
  }
1327
1375
 
1328
- _.each( this.getRelations() || [], function( rel ) {
1329
- delete attributes[ rel.key ];
1330
- });
1376
+ _.each( this.getRelations(), function( rel ) {
1377
+ delete attributes[ rel.key ];
1378
+ });
1331
1379
 
1332
1380
  return new this.constructor( attributes );
1333
1381
  },
1334
-
1382
+
1335
1383
  /**
1336
1384
  * Convert relations to JSON, omits them when required
1337
1385
  */
1338
- toJSON: function(options) {
1386
+ toJSON: function( options ) {
1339
1387
  // If this Model has already been fully serialized in this branch once, return to avoid loops
1340
1388
  if ( this.isLocked() ) {
1341
1389
  return this.id;
@@ -1348,65 +1396,70 @@
1348
1396
  json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue;
1349
1397
  }
1350
1398
 
1351
- _.each( this._relations || [], function( rel ) {
1352
- var value = json[ rel.key ];
1399
+ _.each( this._relations, function( rel ) {
1400
+ var value = json[ rel.key ];
1353
1401
 
1354
- if ( rel.options.includeInJSON === true) {
1355
- if ( value && _.isFunction( value.toJSON ) ) {
1356
- json[ rel.keyDestination ] = value.toJSON( options );
1357
- }
1358
- else {
1359
- json[ rel.keyDestination ] = null;
1360
- }
1402
+ if ( rel.options.includeInJSON === true) {
1403
+ if ( value && _.isFunction( value.toJSON ) ) {
1404
+ json[ rel.keyDestination ] = value.toJSON( options );
1361
1405
  }
1362
- else if ( _.isString( rel.options.includeInJSON ) ) {
1363
- if ( value instanceof Backbone.Collection ) {
1364
- json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON );
1365
- }
1366
- else if ( value instanceof Backbone.Model ) {
1367
- json[ rel.keyDestination ] = value.get( rel.options.includeInJSON );
1368
- }
1369
- else {
1370
- json[ rel.keyDestination ] = null;
1371
- }
1406
+ else {
1407
+ json[ rel.keyDestination ] = null;
1372
1408
  }
1373
- else if ( _.isArray( rel.options.includeInJSON ) ) {
1374
- if ( value instanceof Backbone.Collection ) {
1375
- var valueSub = [];
1376
- value.each( function( model ) {
1377
- var curJson = {};
1378
- _.each( rel.options.includeInJSON, function( key ) {
1379
- curJson[ key ] = model.get( key );
1380
- });
1381
- valueSub.push( curJson );
1382
- });
1383
- json[ rel.keyDestination ] = valueSub;
1384
- }
1385
- else if ( value instanceof Backbone.Model ) {
1386
- var valueSub = {};
1409
+ }
1410
+ else if ( _.isString( rel.options.includeInJSON ) ) {
1411
+ if ( value instanceof Backbone.Collection ) {
1412
+ json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON );
1413
+ }
1414
+ else if ( value instanceof Backbone.Model ) {
1415
+ json[ rel.keyDestination ] = value.get( rel.options.includeInJSON );
1416
+ }
1417
+ else {
1418
+ json[ rel.keyDestination ] = null;
1419
+ }
1420
+ }
1421
+ else if ( _.isArray( rel.options.includeInJSON ) ) {
1422
+ if ( value instanceof Backbone.Collection ) {
1423
+ var valueSub = [];
1424
+ value.each( function( model ) {
1425
+ var curJson = {};
1387
1426
  _.each( rel.options.includeInJSON, function( key ) {
1388
- valueSub[ key ] = value.get( key );
1427
+ curJson[ key ] = model.get( key );
1389
1428
  });
1390
- json[ rel.keyDestination ] = valueSub;
1391
- }
1392
- else {
1393
- json[ rel.keyDestination ] = null;
1394
- }
1429
+ valueSub.push( curJson );
1430
+ });
1431
+ json[ rel.keyDestination ] = valueSub;
1432
+ }
1433
+ else if ( value instanceof Backbone.Model ) {
1434
+ var valueSub = {};
1435
+ _.each( rel.options.includeInJSON, function( key ) {
1436
+ valueSub[ key ] = value.get( key );
1437
+ });
1438
+ json[ rel.keyDestination ] = valueSub;
1395
1439
  }
1396
1440
  else {
1397
- delete json[ rel.key ];
1441
+ json[ rel.keyDestination ] = null;
1398
1442
  }
1443
+ }
1444
+ else {
1445
+ delete json[ rel.key ];
1446
+ }
1399
1447
 
1400
- if ( rel.keyDestination !== rel.key ) {
1401
- delete json[ rel.key ];
1402
- }
1403
- });
1448
+ if ( rel.keyDestination !== rel.key ) {
1449
+ delete json[ rel.key ];
1450
+ }
1451
+ });
1404
1452
 
1405
1453
  this.release();
1406
1454
  return json;
1407
1455
  }
1408
1456
  },
1409
1457
  {
1458
+ /**
1459
+ *
1460
+ * @param superModel
1461
+ * @returns {Backbone.RelationalModel.constructor}
1462
+ */
1410
1463
  setup: function( superModel ) {
1411
1464
  // We don't want to share a relations array with a parent, as this will cause problems with
1412
1465
  // reverse relations.
@@ -1426,29 +1479,34 @@
1426
1479
 
1427
1480
  // Initialize all reverseRelations that belong to this new model.
1428
1481
  _.each( this.prototype.relations || [], function( rel ) {
1429
- if ( !rel.model ) {
1430
- rel.model = this;
1482
+ if ( !rel.model ) {
1483
+ rel.model = this;
1484
+ }
1485
+
1486
+ if ( rel.reverseRelation && rel.model === this ) {
1487
+ var preInitialize = true;
1488
+ if ( _.isString( rel.relatedModel ) ) {
1489
+ /**
1490
+ * The related model might not be defined for two reasons
1491
+ * 1. it is related to itself
1492
+ * 2. it never gets defined, e.g. a typo
1493
+ * 3. the model hasn't been defined yet, but will be later
1494
+ * In neither of these cases do we need to pre-initialize reverse relations.
1495
+ * However, for 3. (which is, to us, indistinguishable from 2.), we do need to attempt
1496
+ * setting up this relation again later, in case the related model is defined later.
1497
+ */
1498
+ var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
1499
+ preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
1431
1500
  }
1432
1501
 
1433
- if ( rel.reverseRelation && rel.model === this ) {
1434
- var preInitialize = true;
1435
- if ( _.isString( rel.relatedModel ) ) {
1436
- /**
1437
- * The related model might not be defined for two reasons
1438
- * 1. it never gets defined, e.g. a typo
1439
- * 2. it is related to itself
1440
- * In neither of these cases do we need to pre-initialize reverse relations.
1441
- */
1442
- var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
1443
- preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
1444
- }
1445
-
1446
- var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
1447
- if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) {
1448
- new type( null, rel );
1449
- }
1502
+ if ( preInitialize ) {
1503
+ Backbone.Relational.store.initializeRelation( null, rel );
1450
1504
  }
1451
- }, this );
1505
+ else if ( _.isString( rel.relatedModel ) ) {
1506
+ Backbone.Relational.store.addOrphanRelation( rel );
1507
+ }
1508
+ }
1509
+ }, this );
1452
1510
 
1453
1511
  return this;
1454
1512
  },
@@ -1478,6 +1536,9 @@
1478
1536
  return new model( attributes, options );
1479
1537
  },
1480
1538
 
1539
+ /**
1540
+ *
1541
+ */
1481
1542
  initializeModelHierarchy: function() {
1482
1543
  // If we're here for the first time, try to determine if this modelType has a 'superModel'.
1483
1544
  if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) {
@@ -1516,24 +1577,27 @@
1516
1577
  /**
1517
1578
  * Find an instance of `this` type in 'Backbone.Relational.store'.
1518
1579
  * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
1519
- * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
1580
+ * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
1520
1581
  * Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).
1521
1582
  * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
1522
1583
  * @param {Object} [options]
1523
1584
  * @param {Boolean} [options.create=true]
1524
- * @param {Boolean} [options.update=true]
1585
+ * @param {Boolean} [options.merge=true]
1586
+ * @param {Boolean} [options.parse=false]
1525
1587
  * @return {Backbone.RelationalModel}
1526
1588
  */
1527
1589
  findOrCreate: function( attributes, options ) {
1528
1590
  options || ( options = {} );
1529
- var parsedAttributes = (_.isObject( attributes ) && this.prototype.parse) ? this.prototype.parse( attributes ) : attributes;
1591
+ var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ?
1592
+ this.prototype.parse( attributes ) : attributes;
1593
+
1530
1594
  // Try to find an instance of 'this' model type in the store
1531
1595
  var model = Backbone.Relational.store.find( this, parsedAttributes );
1532
1596
 
1533
- // If we found an instance, update it with the data in 'item' (unless 'options.update' is false).
1597
+ // If we found an instance, update it with the data in 'item' (unless 'options.merge' is false).
1534
1598
  // If not, create an instance (unless 'options.create' is false).
1535
1599
  if ( _.isObject( attributes ) ) {
1536
- if ( model && options.update !== false ) {
1600
+ if ( model && options.merge !== false ) {
1537
1601
  model.set( parsedAttributes, options );
1538
1602
  }
1539
1603
  else if ( !model && options.create !== false ) {
@@ -1545,10 +1609,12 @@
1545
1609
  }
1546
1610
  });
1547
1611
  _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
1548
-
1612
+
1549
1613
  /**
1550
1614
  * Override Backbone.Collection._prepareModel, so objects will be built using the correct type
1551
1615
  * if the collection.model has subModels.
1616
+ * Attempts to find a model for `attrs` in Backbone.store through `findOrCreate`
1617
+ * (which sets the new properties on it if found), or instantiates a new model.
1552
1618
  */
1553
1619
  Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;
1554
1620
  Backbone.Collection.prototype._prepareModel = function ( attrs, options ) {
@@ -1571,7 +1637,8 @@
1571
1637
  model = new this.model( attrs, options );
1572
1638
  }
1573
1639
 
1574
- if ( !model._validate( attrs, options ) ) {
1640
+ if ( model && model.isNew() && !model._validate( attrs, options ) ) {
1641
+ this.trigger( 'invalid', this, attrs, options );
1575
1642
  model = false;
1576
1643
  }
1577
1644
  }
@@ -1579,67 +1646,86 @@
1579
1646
  return model;
1580
1647
  };
1581
1648
 
1582
-
1649
+
1583
1650
  /**
1584
- * Override Backbone.Collection.add, so objects fetched from the server multiple times will
1585
- * update the existing Model. Also, trigger 'relational:add'.
1651
+ * Override Backbone.Collection.add, so we'll create objects from attributes where required,
1652
+ * and update the existing models. Also, trigger 'relational:add'.
1586
1653
  */
1587
1654
  var add = Backbone.Collection.prototype.__add = Backbone.Collection.prototype.add;
1588
1655
  Backbone.Collection.prototype.add = function( models, options ) {
1589
- options || (options = {});
1590
- if ( !_.isArray( models ) ) {
1591
- models = [ models ];
1656
+ // Short-circuit if this Collection doesn't hold RelationalModels
1657
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1658
+ return add.apply( this, arguments );
1592
1659
  }
1593
1660
 
1594
- var modelsToAdd = [];
1661
+ models = _.isArray( models ) ? models.slice() : [ models ];
1662
+ // Set default options to the same values as `add` uses, so `findOrCreate` will also respect those.
1663
+ options = _.extend( { merge: false }, options );
1664
+
1665
+ var newModels = [],
1666
+ toAdd = [];
1595
1667
 
1596
1668
  //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
1597
- _.each( models || [], function( model ) {
1669
+ _.each( models, function( model ) {
1598
1670
  if ( !( model instanceof Backbone.Model ) ) {
1599
- // `_prepareModel` attempts to find `model` in Backbone.store through `findOrCreate`,
1600
- // and sets the new properties on it if is found. Otherwise, a new model is instantiated.
1601
1671
  model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
1602
1672
  }
1603
1673
 
1604
- if ( model instanceof Backbone.Model && !this.get( model ) ) {
1605
- modelsToAdd.push( model );
1674
+ if ( model ) {
1675
+ toAdd.push( model );
1676
+
1677
+ if ( !( this.get( model ) || this.get( model.cid ) ) ) {
1678
+ newModels.push( model );
1606
1679
  }
1607
- }, this );
1680
+ // If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`),
1681
+ // we may get here before `_onModelEvent` has had the chance to update `_byId`.
1682
+ else if ( model.id != null ) {
1683
+ this._byId[ model.id ] = model;
1684
+ }
1685
+ }
1686
+ }, this );
1608
1687
 
1609
1688
  // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
1610
- if ( modelsToAdd.length ) {
1611
- add.call( this, modelsToAdd, options );
1689
+ add.call( this, toAdd, options );
1612
1690
 
1613
- _.each( modelsToAdd || [], function( model ) {
1691
+ _.each( newModels, function( model ) {
1692
+ // Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection.
1693
+ if ( this.get( model ) || this.get( model.cid ) ) {
1614
1694
  this.trigger( 'relational:add', model, this, options );
1615
- }, this );
1616
- }
1695
+ }
1696
+ }, this );
1617
1697
 
1618
1698
  return this;
1619
1699
  };
1620
-
1700
+
1621
1701
  /**
1622
1702
  * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
1623
1703
  */
1624
1704
  var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove;
1625
1705
  Backbone.Collection.prototype.remove = function( models, options ) {
1626
- options || (options = {});
1627
- if ( !_.isArray( models ) ) {
1628
- models = [ models ];
1629
- }
1630
- else {
1631
- models = models.slice( 0 );
1706
+ // Short-circuit if this Collection doesn't hold RelationalModels
1707
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1708
+ return remove.apply( this, arguments );
1632
1709
  }
1633
1710
 
1711
+ models = _.isArray( models ) ? models.slice() : [ models ];
1712
+ options || ( options = {} );
1713
+
1714
+ var toRemove = [];
1715
+
1634
1716
  //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
1635
- _.each( models || [], function( model ) {
1636
- model = this.get( model );
1717
+ _.each( models, function( model ) {
1718
+ model = this.get( model ) || this.get( model.cid );
1719
+ model && toRemove.push( model );
1720
+ }, this );
1637
1721
 
1638
- if ( model instanceof Backbone.Model ) {
1639
- remove.call( this, model, options );
1640
- this.trigger('relational:remove', model, this, options);
1641
- }
1722
+ if ( toRemove.length ) {
1723
+ remove.call( this, toRemove, options );
1724
+
1725
+ _.each( toRemove, function( model ) {
1726
+ this.trigger('relational:remove', model, this, options);
1642
1727
  }, this );
1728
+ }
1643
1729
 
1644
1730
  return this;
1645
1731
  };
@@ -1650,7 +1736,10 @@
1650
1736
  var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset;
1651
1737
  Backbone.Collection.prototype.reset = function( models, options ) {
1652
1738
  reset.call( this, models, options );
1653
- this.trigger( 'relational:reset', this, options );
1739
+
1740
+ if ( this.model.prototype instanceof Backbone.RelationalModel ) {
1741
+ this.trigger( 'relational:reset', this, options );
1742
+ }
1654
1743
 
1655
1744
  return this;
1656
1745
  };
@@ -1661,32 +1750,39 @@
1661
1750
  var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort;
1662
1751
  Backbone.Collection.prototype.sort = function( options ) {
1663
1752
  sort.call( this, options );
1664
- this.trigger( 'relational:reset', this, options );
1753
+
1754
+ if ( this.model.prototype instanceof Backbone.RelationalModel ) {
1755
+ this.trigger( 'relational:reset', this, options );
1756
+ }
1665
1757
 
1666
1758
  return this;
1667
1759
  };
1668
-
1760
+
1669
1761
  /**
1670
1762
  * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
1671
1763
  * are ready.
1672
1764
  */
1673
1765
  var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger;
1674
1766
  Backbone.Collection.prototype.trigger = function( eventName ) {
1767
+ // Short-circuit if this Collection doesn't hold RelationalModels
1768
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1769
+ return trigger.apply( this, arguments );
1770
+ }
1771
+
1675
1772
  if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) {
1676
- var dit = this, args = arguments;
1773
+ var dit = this,
1774
+ args = arguments;
1677
1775
 
1678
- if (eventName === 'add') {
1776
+ if ( _.isObject( args[ 3 ] ) ) {
1679
1777
  args = _.toArray( args );
1680
- // the fourth argument in case of a regular add is the option object.
1778
+ // the fourth argument is the option object.
1681
1779
  // we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
1682
- if (_.isObject( args[3] ) ) {
1683
- args[3] = _.clone( args[3] );
1684
- }
1780
+ args[ 3 ] = _.clone( args[ 3 ] );
1685
1781
  }
1686
1782
 
1687
1783
  Backbone.Relational.eventQueue.add( function() {
1688
- trigger.apply( dit, args );
1689
- });
1784
+ trigger.apply( dit, args );
1785
+ });
1690
1786
  }
1691
1787
  else {
1692
1788
  trigger.apply( this, arguments );