backbone-relational-rails 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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.4.0 == Backbone-relational 0.4.0
21
+ backbone-relational-rails 0.5.0 == Backbone-relational 0.5.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.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
6
6
  end
7
7
  end
@@ -1,27 +1,30 @@
1
1
  /**
2
- * Backbone-relational.js 0.4.0
2
+ * Backbone-relational.js 0.5.0
3
3
  * (c) 2011 Paul Uithol
4
4
  *
5
5
  * Backbone-relational may be freely distributed under the MIT license.
6
6
  * For details and documentation: https://github.com/PaulUithol/Backbone-relational.
7
7
  * Depends on (as in, compeletely useless without) Backbone: https://github.com/documentcloud/backbone.
8
8
  */
9
- (function(undefined) {
10
-
9
+ ( function( undefined ) {
11
10
  /**
12
11
  * CommonJS shim
13
12
  **/
14
- if (typeof window === 'undefined') {
15
- var _ = require('underscore');
16
- var Backbone = require('backbone');
17
- var exports = module.exports = Backbone;
18
- } else {
19
- var _ = this._;
20
- var Backbone = this.Backbone;
21
- var exports = this;
13
+ var _, Backbone, exports;
14
+ if ( typeof window === 'undefined' ) {
15
+ _ = require( 'underscore' );
16
+ Backbone = require( 'backbone' );
17
+ exports = module.exports = Backbone;
18
+ }
19
+ else {
20
+ var _ = window._;
21
+ Backbone = window.Backbone;
22
+ exports = window;
22
23
  }
23
24
 
24
- Backbone.Relational = {};
25
+ Backbone.Relational = {
26
+ showWarnings: true
27
+ };
25
28
 
26
29
  /**
27
30
  * Semaphore mixin; can be used as both binary and counting.
@@ -32,7 +35,7 @@
32
35
 
33
36
  acquire: function() {
34
37
  if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {
35
- throw new Error('Max permits acquired');
38
+ throw new Error( 'Max permits acquired' );
36
39
  }
37
40
  else {
38
41
  this._permitsUsed++;
@@ -41,7 +44,7 @@
41
44
 
42
45
  release: function() {
43
46
  if ( this._permitsUsed === 0 ) {
44
- throw new Error('All permits released');
47
+ throw new Error( 'All permits released' );
45
48
  }
46
49
  else {
47
50
  this._permitsUsed--;
@@ -54,7 +57,7 @@
54
57
 
55
58
  setAvailablePermits: function( amount ) {
56
59
  if ( this._permitsUsed > amount ) {
57
- throw new Error('Available permits cannot be less than used permits');
60
+ throw new Error( 'Available permits cannot be less than used permits' );
58
61
  }
59
62
  this._permitsAvailable = amount;
60
63
  }
@@ -122,8 +125,11 @@
122
125
  /**
123
126
  * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
124
127
  * existing instances of 'model' in the store as well.
125
- * @param {object} relation; required properties:
126
- * - model, type, key and relatedModel
128
+ * @param {object} relation
129
+ * @param {Backbone.RelationalModel} relation.model
130
+ * @param {String} relation.type
131
+ * @param {String} relation.key
132
+ * @param {String|object} relation.relatedModel
127
133
  */
128
134
  addReverseRelation: function( relation ) {
129
135
  var exists = _.any( this._reverseRelations, function( rel ) {
@@ -146,23 +152,24 @@
146
152
 
147
153
  /**
148
154
  * Add a 'relation' to all existing instances of 'relation.model' in the store
155
+ * @param {object} relation
149
156
  */
150
157
  retroFitRelation: function( relation ) {
151
158
  var coll = this.getCollection( relation.model );
152
159
  coll.each( function( model ) {
153
- var rel = new relation.type( model, relation );
160
+ new relation.type( model, relation );
154
161
  }, this);
155
162
  },
156
163
 
157
164
  /**
158
165
  * Find the Store's collection for a certain type of model.
159
- * @param model {Backbone.RelationalModel}
166
+ * @param {Backbone.RelationalModel} model
160
167
  * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
161
168
  */
162
169
  getCollection: function( model ) {
163
170
  var coll = _.detect( this._collections, function( c ) {
164
171
  // Check if model is the type itself (a ref to the constructor), or is of type c.model
165
- return model === c.model || model instanceof c.model;
172
+ return model === c.model || model.constructor === c.model;
166
173
  });
167
174
 
168
175
  if ( !coll ) {
@@ -174,10 +181,11 @@
174
181
 
175
182
  /**
176
183
  * Find a type on the global object by name. Splits name on dots.
177
- * @param {string} name
184
+ * @param {String} name
185
+ * @return {object}
178
186
  */
179
187
  getObjectByName: function( name ) {
180
- var type = _.reduce( name.split('.'), function( memo, val ) {
188
+ var type = _.reduce( name.split( '.' ), function( memo, val ) {
181
189
  return memo[ val ];
182
190
  }, exports);
183
191
  return type !== exports ? type: null;
@@ -209,16 +217,19 @@
209
217
 
210
218
  /**
211
219
  * Add a 'model' to it's appropriate collection. Retain the original contents of 'model.collection'.
220
+ * @param {Backbone.RelationalModel} model
212
221
  */
213
222
  register: function( model ) {
214
223
  var modelColl = model.collection;
215
224
  var coll = this.getCollection( model );
216
- coll && coll._add( model );
225
+ coll && coll.add( model );
226
+ model.bind( 'destroy', this.unregister, this );
217
227
  model.collection = modelColl;
218
228
  },
219
229
 
220
230
  /**
221
231
  * Explicitly update a model's id in it's store collection
232
+ * @param {Backbone.RelationalModel} model
222
233
  */
223
234
  update: function( model ) {
224
235
  var coll = this.getCollection( model );
@@ -227,8 +238,10 @@
227
238
 
228
239
  /**
229
240
  * Remove a 'model' from the store.
241
+ * @param {Backbone.RelationalModel} model
230
242
  */
231
243
  unregister: function( model ) {
244
+ model.unbind( 'destroy', this.unregister );
232
245
  var coll = this.getCollection( model );
233
246
  coll && coll.remove( model );
234
247
  }
@@ -236,68 +249,73 @@
236
249
  Backbone.Relational.store = new Backbone.Store();
237
250
 
238
251
  /**
239
- * The main Relation class, from which 'HasOne' and 'HasMany' inherit.
252
+ * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events
253
+ * are used to regulate addition and removal of models from relations.
254
+ *
240
255
  * @param {Backbone.RelationalModel} instance
241
- * @param {object} options.
242
- * Required properties:
243
- * - {string} key
244
- * - {Backbone.RelationalModel.constructor} relatedModel
245
- * Optional properties:
246
- * - {bool} includeInJSON: create objects from the contents of keys if the object is not found in Backbone.store.
247
- * - {bool} createModels: serialize the attributes for related model(s)' in toJSON on create/update, or just their ids.
248
- * - {object} reverseRelation: Specify a bi-directional relation. If provided, Relation will reciprocate
249
- * the relation to the 'relatedModel'. Required and optional properties match 'options', except for:
250
- * - {Backbone.Relation|string} type: 'HasOne' or 'HasMany'
256
+ * @param {object} options
257
+ * @param {string} options.key
258
+ * @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.
260
+ * @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
262
+ * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
263
+ * {Backbone.Relation|String} type ('HasOne' or 'HasMany').
251
264
  */
252
265
  Backbone.Relation = function( instance, options ) {
253
266
  this.instance = instance;
254
-
255
267
  // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
256
268
  options = ( typeof options === 'object' && options ) || {};
257
269
  this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
258
270
  this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
259
- Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
271
+ Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
272
+ this.model = options.model || this.instance.constructor;
260
273
  this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
261
274
 
262
275
  this.key = this.options.key;
263
- this.keyContents = this.instance.get( this.key );
264
-
276
+
265
277
  // 'exports' should be the global object where 'relatedModel' can be found on if given as a string.
266
278
  this.relatedModel = this.options.relatedModel;
267
279
  if ( _.isString( this.relatedModel ) ) {
268
280
  this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel );
269
281
  }
270
-
282
+
271
283
  if ( !this.checkPreconditions() ) {
272
284
  return false;
273
285
  }
274
-
275
- // Add this Relation to instance._relations
276
- this.instance._relations.push( this );
277
-
286
+
287
+ if(instance) {
288
+ this.keyContents = this.instance.get( this.key );
289
+ // Add this Relation to instance._relations
290
+ this.instance._relations.push( this );
291
+ }
292
+
278
293
  // Add the reverse relation on 'relatedModel' to the store's reverseRelations
279
294
  if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
280
295
  Backbone.Relational.store.addReverseRelation( _.defaults( {
281
296
  isAutoRelation: true,
282
297
  model: this.relatedModel,
283
- relatedModel: this.instance.constructor,
298
+ relatedModel: this.model,
284
299
  reverseRelation: this.options // current relation is the 'reverseRelation' for it's own reverseRelation
285
300
  },
286
301
  this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
287
302
  ) );
288
303
  }
289
-
290
- this.initialize();
291
-
304
+
292
305
  _.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' );
293
- // When a model in the store is destroyed, check if it is 'this.instance'.
294
- Backbone.Relational.store.getCollection( this.instance )
295
- .bind( 'relational:remove', this._modelRemovedFromCollection );
296
-
297
- // When 'relatedModel' are created or destroyed, check if it affects this relation.
298
- Backbone.Relational.store.getCollection( this.relatedModel )
299
- .bind( 'relational:add', this._relatedModelAdded )
300
- .bind( 'relational:remove', this._relatedModelRemoved );
306
+
307
+ if( instance ) {
308
+ this.initialize();
309
+
310
+ // When a model in the store is destroyed, check if it is 'this.instance'.
311
+ Backbone.Relational.store.getCollection( this.instance )
312
+ .bind( 'relational:remove', this._modelRemovedFromCollection );
313
+
314
+ // When 'relatedModel' are created or destroyed, check if it affects this relation.
315
+ Backbone.Relational.store.getCollection( this.relatedModel )
316
+ .bind( 'relational:add', this._relatedModelAdded )
317
+ .bind( 'relational:remove', this._relatedModelRemoved );
318
+ }
301
319
  };
302
320
  // Fix inheritance :\
303
321
  Backbone.Relation.extend = Backbone.Model.extend;
@@ -337,41 +355,44 @@
337
355
 
338
356
  /**
339
357
  * Check several pre-conditions.
340
- * @return {bool} True if pre-conditions are satisfied, false if they're not.
358
+ * @return {Boolean} True if pre-conditions are satisfied, false if they're not.
341
359
  */
342
360
  checkPreconditions: function() {
343
- var i = this.instance, k = this.key, rm = this.relatedModel;
344
- if ( !i || !k || !rm ) {
345
- console && console.warn( 'Relation=%o; no instance, key or relatedModel (%o, %o, %o)', this, i, k, rm );
361
+ var i = this.instance, k = this.key, m = this.model, rm = this.relatedModel, warn = Backbone.Relational.showWarnings;
362
+ if ( !m || !k || !rm ) {
363
+ warn && typeof console !== 'undefined' && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, k, rm );
346
364
  return false;
347
365
  }
348
- // Check if 'instance' is a Backbone.RelationalModel
349
- if ( !( i instanceof Backbone.RelationalModel ) ) {
350
- console && console.warn( 'Relation=%o; instance=%o is not a Backbone.RelationalModel', this, i );
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 );
351
369
  return false;
352
370
  }
353
371
  // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
354
372
  if ( !( rm.prototype instanceof Backbone.RelationalModel.prototype.constructor ) ) {
355
- console && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
373
+ warn && typeof console !== 'undefined' && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
356
374
  return false;
357
375
  }
358
376
  // Check if this is not a HasMany, and the reverse relation is HasMany as well
359
377
  if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany.prototype.constructor ) {
360
- console && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
378
+ warn && typeof console !== 'undefined' && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
361
379
  return false;
362
380
  }
363
- // Check if we're not attempting to create a duplicate relationship
364
- if ( i._relations.length ) {
365
- var exists = _.any( i._relations, function( rel ) {
366
- var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key;
367
- return rel.relatedModel === rm && rel.key === k
368
- && ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key );
369
- }, this );
370
-
371
- if ( exists ) {
372
- console && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
381
+
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 ) {
386
+ var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key;
387
+ return rel.relatedModel === rm && rel.key === k &&
388
+ ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key );
389
+ }, this );
390
+
391
+ if ( exists ) {
392
+ warn && typeof console !== 'undefined' && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
373
393
  this, i, k, rm, this.reverseRelation.key );
374
- return false;
394
+ return false;
395
+ }
375
396
  }
376
397
  }
377
398
  return true;
@@ -397,8 +418,8 @@
397
418
  * relation of the current one.
398
419
  */
399
420
  _isReverseRelation: function( relation ) {
400
- if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key
401
- && this.key === relation.reverseRelation.key ) {
421
+ if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
422
+ this.key === relation.reverseRelation.key ) {
402
423
  return true;
403
424
  }
404
425
  return false;
@@ -406,8 +427,8 @@
406
427
 
407
428
  /**
408
429
  * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
409
- * @param model {Backbone.RelationalModel} Optional; get the reverse relations for a specific model.
410
- * If not specified, 'this.related' is used.
430
+ * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
431
+ * If not specified, 'this.related' is used.
411
432
  */
412
433
  getReverseRelations: function( model ) {
413
434
  var reverseRelations = [];
@@ -459,19 +480,20 @@
459
480
 
460
481
  initialize: function() {
461
482
  _.bindAll( this, 'onChange' );
483
+
462
484
  this.instance.bind( 'relational:change:' + this.key, this.onChange );
463
-
464
- var model = this.findRelated();
485
+
486
+ var model = this.findRelated( { silent: true } );
465
487
  this.setRelated( model );
466
-
488
+
467
489
  // Notify new 'related' object of the new relation.
468
490
  var dit = this;
469
491
  _.each( dit.getReverseRelations(), function( relation ) {
470
- relation.addRelated( dit.instance );
471
- } );
492
+ relation.addRelated( dit.instance );
493
+ } );
472
494
  },
473
495
 
474
- findRelated: function() {
496
+ findRelated: function( options ) {
475
497
  var item = this.keyContents;
476
498
  var model = null;
477
499
 
@@ -481,7 +503,13 @@
481
503
  else if ( item && ( _.isString( item ) || _.isNumber( item ) || typeof( item ) === 'object' ) ) {
482
504
  // Try to find an instance of the appropriate 'relatedModel' in the store, or create it
483
505
  var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
484
- model = Backbone.Relational.store.find( this.relatedModel, id ) || this.createModel( item );
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
+ }
485
513
  }
486
514
 
487
515
  return model;
@@ -512,7 +540,7 @@
512
540
  this.related = attr;
513
541
  }
514
542
  else if ( attr ) {
515
- var related = this.findRelated();
543
+ var related = this.findRelated( options );
516
544
  this.setRelated( related );
517
545
  }
518
546
  else {
@@ -588,11 +616,12 @@
588
616
 
589
617
  options: {
590
618
  reverseRelation: { type: 'HasOne' },
591
- collectionType: Backbone.Collection
619
+ collectionType: Backbone.Collection,
620
+ collectionKey: true
592
621
  },
593
622
 
594
623
  initialize: function() {
595
- _.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval' );
624
+ _.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval', 'handleReset' );
596
625
  this.instance.bind( 'relational:change:' + this.key, this.onChange );
597
626
 
598
627
  // Handle a custom 'collectionType'
@@ -605,29 +634,60 @@
605
634
  }
606
635
 
607
636
  this.setRelated( this.prepareCollection( new this.collectionType() ) );
608
- this.findRelated();
637
+ this.findRelated( { silent: true } );
609
638
  },
610
639
 
611
640
  prepareCollection: function( collection ) {
612
641
  if ( this.related ) {
613
- this.related.unbind( 'relational:add', this.handleAddition ).unbind('relational:remove', this.handleRemoval );
642
+ this.related
643
+ .unbind( 'relational:add', this.handleAddition )
644
+ .unbind( 'relational:remove', this.handleRemoval )
645
+ .unbind( 'relational:reset', this.handleReset )
614
646
  }
615
647
 
616
648
  collection.reset();
617
649
  collection.model = this.relatedModel;
618
- collection.bind( 'relational:add', this.handleAddition ).bind('relational:remove', this.handleRemoval );
650
+
651
+ if ( this.options.collectionKey ) {
652
+ var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey;
653
+
654
+ if (collection[ key ] && collection[ key ] !== this.instance ) {
655
+ if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
656
+ console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey );
657
+ }
658
+ }
659
+ else {
660
+ collection[ key ] = this.instance;
661
+ }
662
+ }
663
+
664
+ collection
665
+ .bind( 'relational:add', this.handleAddition )
666
+ .bind( 'relational:remove', this.handleRemoval )
667
+ .bind( 'relational:reset', this.handleReset );
668
+
619
669
  return collection;
620
670
  },
621
671
 
622
- findRelated: function() {
623
- if ( this.keyContents && _.isArray( this.keyContents ) ) {
672
+ findRelated: function( options ) {
673
+ 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 ];
676
+
624
677
  // Try to find instances of the appropriate 'relatedModel' in the store
625
678
  _.each( this.keyContents, function( item ) {
626
679
  var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
627
- var model = Backbone.Relational.store.find( this.relatedModel, id ) || this.createModel( item );
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
+ }
628
688
 
629
689
  if ( model && !this.related.getByCid( model ) && !this.related.get( model ) ) {
630
- this.related._add( model );
690
+ this.related.add( model );
631
691
  }
632
692
  }, this);
633
693
  }
@@ -650,12 +710,12 @@
650
710
  this.prepareCollection( attr );
651
711
  this.related = attr;
652
712
  }
653
- // Otherwise, 'attr' should be an array of related object ids
713
+ // Otherwise, 'attr' should be an array of related object ids.
714
+ // Re-use the current 'this.related' if it is a Backbone.Collection.
654
715
  else {
655
- // Re-use the current 'this.related' if it is a Backbone.Collection
656
716
  var coll = this.related instanceof Backbone.Collection ? this.related : new this.collectionType();
657
717
  this.setRelated( this.prepareCollection( coll ) );
658
- this.findRelated();
718
+ this.findRelated( options );
659
719
  }
660
720
 
661
721
  // Notify new 'related' object of the new relation
@@ -679,7 +739,7 @@
679
739
  }, this );
680
740
 
681
741
  if ( item ) {
682
- this.related._add( model, options );
742
+ this.related.add( model, options );
683
743
  }
684
744
  }
685
745
  },
@@ -690,14 +750,20 @@
690
750
  */
691
751
  handleAddition: function( model, coll, options ) {
692
752
  //console.debug('handleAddition called; args=%o', arguments);
753
+ // Make sure the model is in fact a valid model before continuing.
754
+ // (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel)
755
+ if( !( model instanceof Backbone.Model ) ) {
756
+ return;
757
+ }
758
+
693
759
  options = this.sanitizeOptions( options );
694
- var dit = this;
695
760
 
696
761
  _.each( this.getReverseRelations( model ), function( relation ) {
697
762
  relation.addRelated( this.instance, options );
698
763
  }, this );
699
-
764
+
700
765
  // Only trigger 'add' once the newly added model is initialized (so, has it's relations set up)
766
+ var dit = this;
701
767
  Backbone.Relational.eventQueue.add( function() {
702
768
  !options.silentChange && dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
703
769
  });
@@ -709,6 +775,10 @@
709
775
  */
710
776
  handleRemoval: function( model, coll, options ) {
711
777
  //console.debug('handleRemoval called; args=%o', arguments);
778
+ if( !( model instanceof Backbone.Model ) ) {
779
+ return;
780
+ }
781
+
712
782
  options = this.sanitizeOptions( options );
713
783
 
714
784
  _.each( this.getReverseRelations( model ), function( relation ) {
@@ -720,12 +790,21 @@
720
790
  !options.silentChange && dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
721
791
  });
722
792
  },
793
+
794
+ handleReset: function( coll, options ) {
795
+ options = this.sanitizeOptions( options );
796
+
797
+ var dit = this;
798
+ Backbone.Relational.eventQueue.add( function() {
799
+ !options.silentChange && dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
800
+ });
801
+ },
723
802
 
724
803
  addRelated: function( model, options ) {
725
804
  var dit = this;
726
805
  model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice
727
806
  if ( dit.related && !dit.related.getByCid( model ) && !dit.related.get( model ) ) {
728
- dit.related._add( model, options );
807
+ dit.related.add( model, options );
729
808
  }
730
809
  });
731
810
  },
@@ -738,7 +817,8 @@
738
817
  });
739
818
 
740
819
  /**
741
- * New events:
820
+ * A type of Backbone.Model that also maintains relations to other models and collections.
821
+ * New events when compared to the original:
742
822
  * - 'add:<key>' (model, related collection, options)
743
823
  * - 'remove:<key>' (model, related collection, options)
744
824
  * - 'update:<key>' (model, related model or collection, options)
@@ -753,7 +833,7 @@
753
833
  constructor: function( attributes, options ) {
754
834
  // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
755
835
  // Defer 'processQueue', so that when 'Relation.createModels' is used we:
756
- // a) Survive 'Backbone.Collection._add'; this takes care we won't error on "can't add model to a set twice"
836
+ // a) Survive 'Backbone.Collection.add'; this takes care we won't error on "can't add model to a set twice"
757
837
  // (by creating a model from properties, having the model add itself to the collection via one of
758
838
  // it's relations, then trying to add it to the collection).
759
839
  // b) Trigger 'HasMany' collection events only after the model is really fully set up.
@@ -762,7 +842,7 @@
762
842
  if ( options && options.collection ) {
763
843
  this._deferProcessing = true;
764
844
 
765
- var processQueue = function( model, coll ) {
845
+ var processQueue = function( model ) {
766
846
  if ( model === dit ) {
767
847
  dit._deferProcessing = false;
768
848
  dit.processQueue();
@@ -805,8 +885,8 @@
805
885
  },
806
886
 
807
887
  /**
808
- * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then create a new instance.
809
- * The regular constructor (which fills this.attributes, initialize, etc) hasn't run yet at this time!
888
+ * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance.
889
+ * Invoked in the first call so 'set' (which is made from the Backbone.Model constructor).
810
890
  */
811
891
  initializeRelations: function() {
812
892
  this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
@@ -818,7 +898,7 @@
818
898
  new type( this, rel ); // Also pushes the new Relation into _relations
819
899
  }
820
900
  else {
821
- console && console.warn( 'Relation=%o; missing or invalid type!', rel );
901
+ Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid type!', rel );
822
902
  }
823
903
  }, this );
824
904
 
@@ -861,19 +941,19 @@
861
941
  /**
862
942
  * Get a specific relation.
863
943
  * @param key {string} The relation key to look for.
864
- * @return {Backbone.Relation|null} An instance of 'Backbone.Relation', if a relation was found for 'key', or null.
944
+ * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null.
865
945
  */
866
946
  getRelation: function( key ) {
867
947
  return _.detect( this._relations, function( rel ) {
868
- if ( rel.key === key ) {
869
- return true;
870
- }
871
- }, this );
948
+ if ( rel.key === key ) {
949
+ return true;
950
+ }
951
+ }, this );
872
952
  },
873
953
 
874
954
  /**
875
955
  * Get all of the created relations.
876
- * @return {array of Backbone.Relation}
956
+ * @return {Backbone.Relation[]}
877
957
  */
878
958
  getRelations: function() {
879
959
  return this._relations;
@@ -883,77 +963,95 @@
883
963
  * Retrieve related objects.
884
964
  * @param key {string} The relation key to fetch models for.
885
965
  * @param options {object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
886
- * @return {xhr} An array or request objects
966
+ * @return {jQuery.when[]} An array of request objects
887
967
  */
888
968
  fetchRelated: function( key, options ) {
889
969
  options || ( options = {} );
890
- var rel = this.getRelation( key ),
970
+ var setUrl,
971
+ requests = [],
972
+ rel = this.getRelation( key ),
891
973
  keyContents = rel && rel.keyContents,
892
974
  toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) {
893
- var id = _.isString( item ) || _.isNumber( item ) ? item : item[ rel.relatedModel.prototype.idAttribute ];
894
- return id && !Backbone.Relational.store.find( rel.relatedModel, id );
895
- }, this );
975
+ var id = _.isString( item ) || _.isNumber( item ) ? item : item[ rel.relatedModel.prototype.idAttribute ];
976
+ return id && !Backbone.Relational.store.find( rel.relatedModel, id );
977
+ }, this );
896
978
 
897
979
  if ( toFetch && toFetch.length ) {
898
980
  // Create a model for each entry in 'keyContents' that is to be fetched
899
981
  var models = _.map( toFetch, function( item ) {
900
- if ( typeof( item ) === 'object' ) {
901
- var model = new rel.relatedModel( item );
902
- }
903
- else {
904
- var attrs = {};
905
- attrs[ rel.relatedModel.prototype.idAttribute ] = item;
906
- var model = new rel.relatedModel( attrs );
907
- }
908
- return model;
909
- }, this );
982
+ var model;
983
+
984
+ if ( typeof( item ) === 'object' ) {
985
+ model = new rel.relatedModel( item );
986
+ }
987
+ else {
988
+ var attrs = {};
989
+ attrs[ rel.relatedModel.prototype.idAttribute ] = item;
990
+ model = new rel.relatedModel( attrs );
991
+ }
992
+
993
+ return model;
994
+ }, this );
910
995
 
911
996
  // Try if the 'collection' can provide a url to fetch a set of models in one request.
912
997
  if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) {
913
- var setUrl = rel.related.url( models );
998
+ setUrl = rel.related.url( models );
914
999
  }
915
1000
 
916
1001
  // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls.
917
1002
  // To make sure it can, test if the url we got by supplying a list of models to fetch is different from
918
1003
  // the one supplied for the default fetch action (without args to 'url').
919
1004
  if ( setUrl && setUrl !== rel.related.url() ) {
920
- var opts = _.defaults( {
1005
+ var opts = _.defaults(
1006
+ {
921
1007
  error: function() {
922
- var args = arguments;
923
- _.each( models, function( model ) {
924
- model.destroy();
925
- options.error && options.error.apply( model, args );
926
- })
927
- },
1008
+ var args = arguments;
1009
+ _.each( models, function( model ) {
1010
+ model.trigger( 'destroy', model, model.collection, options );
1011
+ options.error && options.error.apply( model, args );
1012
+ });
1013
+ },
928
1014
  url: setUrl
929
1015
  },
930
1016
  options,
931
1017
  { add: true }
932
1018
  );
933
-
934
- var requests = [ rel.related.fetch( opts ) ];
1019
+
1020
+ requests = [ rel.related.fetch( opts ) ];
935
1021
  }
936
1022
  else {
937
- var requests = _.map( models, function( model ) {
938
- var opts = _.defaults( {
1023
+ requests = _.map( models, function( model ) {
1024
+ var opts = _.defaults(
1025
+ {
939
1026
  error: function() {
940
- model.destroy();
941
- options.error && options.error.apply( model, arguments );
942
- }
943
- },
944
- options
945
- );
946
- return model.fetch( opts );
947
- }, this );
1027
+ model.trigger( 'destroy', model, model.collection, options );
1028
+ options.error && options.error.apply( model, arguments );
1029
+ }
1030
+ },
1031
+ options
1032
+ );
1033
+ return model.fetch( opts );
1034
+ }, this );
948
1035
  }
949
1036
  }
950
1037
 
951
- return _.isUndefined( requests ) ? [] : requests;
1038
+ return requests;
952
1039
  },
953
1040
 
954
- set: function( attributes, options ) {
1041
+ set: function( key, value, options ) {
955
1042
  Backbone.Relational.eventQueue.block();
956
1043
 
1044
+ // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
1045
+ var attributes;
1046
+ if (_.isObject( key ) || key == null) {
1047
+ attributes = key;
1048
+ options = value;
1049
+ }
1050
+ else {
1051
+ attributes = {};
1052
+ attributes[ key ] = value;
1053
+ }
1054
+
957
1055
  var result = Backbone.Model.prototype.set.apply( this, arguments );
958
1056
 
959
1057
  // 'set' is called quite late in 'Backbone.Model.prototype.constructor', but before 'initialize'.
@@ -967,7 +1065,9 @@
967
1065
  Backbone.Relational.store.update( this );
968
1066
  }
969
1067
 
970
- this.updateRelations( options );
1068
+ if ( attributes ) {
1069
+ this.updateRelations( options );
1070
+ }
971
1071
 
972
1072
  // Try to run the global queue holding external events
973
1073
  Backbone.Relational.eventQueue.unblock();
@@ -975,7 +1075,7 @@
975
1075
  return result;
976
1076
  },
977
1077
 
978
- unset: function( attributes, options ) {
1078
+ unset: function( attribute, options ) {
979
1079
  Backbone.Relational.eventQueue.block();
980
1080
 
981
1081
  var result = Backbone.Model.prototype.unset.apply( this, arguments );
@@ -1009,10 +1109,18 @@
1009
1109
  Backbone.Model.prototype.change.apply( dit, arguments );
1010
1110
  });
1011
1111
  },
1012
-
1013
- destroy: function( options ) {
1014
- Backbone.Relational.store.unregister( this );
1015
- return Backbone.Model.prototype.destroy.call( this, options );
1112
+
1113
+ clone: function() {
1114
+ var attributes = _.clone( this.attributes );
1115
+ if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) {
1116
+ attributes[ this.idAttribute ] = null;
1117
+ }
1118
+
1119
+ _.each( this.getRelations(), function( rel ) {
1120
+ delete attributes[ rel.key ];
1121
+ });
1122
+
1123
+ return new this.constructor( attributes );
1016
1124
  },
1017
1125
 
1018
1126
  /**
@@ -1024,15 +1132,14 @@
1024
1132
  return this.id;
1025
1133
  }
1026
1134
 
1135
+ this.acquire();
1027
1136
  var json = Backbone.Model.prototype.toJSON.call( this );
1028
1137
 
1029
1138
  _.each( this._relations, function( rel ) {
1030
1139
  var value = json[ rel.key ];
1031
1140
 
1032
1141
  if ( rel.options.includeInJSON === true && value && _.isFunction( value.toJSON ) ) {
1033
- this.acquire();
1034
1142
  json[ rel.key ] = value.toJSON();
1035
- this.release();
1036
1143
  }
1037
1144
  else if ( _.isString( rel.options.includeInJSON ) ) {
1038
1145
  if ( value instanceof Backbone.Collection ) {
@@ -1047,44 +1154,78 @@
1047
1154
  }
1048
1155
  }, this );
1049
1156
 
1157
+ this.release();
1050
1158
  return json;
1051
1159
  }
1052
1160
  });
1053
1161
  _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
1054
1162
 
1055
1163
  /**
1056
- * Override Backbone.Collection._add, so objects fetched from the server multiple times will
1057
- * update the existing Model. Also, trigger 'relation:add'.
1164
+ * Override Backbone.Collection.add, so objects fetched from the server multiple times will
1165
+ * update the existing Model. Also, trigger 'relational:add'.
1058
1166
  */
1059
- var _add = Backbone.Collection.prototype._add;
1060
- Backbone.Collection.prototype._add = function( model, options ) {
1061
- if ( !( model instanceof Backbone.Model ) ) {
1062
- // Try to find 'model' in Backbone.store. If it already exists, set the new properties on it.
1063
- var found = Backbone.Relational.store.find( this.model, model[ this.model.prototype.idAttribute ] );
1064
- if ( found ) {
1065
- model = found.set( model, options );
1066
- }
1067
- }
1068
-
1069
- //console.debug( 'calling _add on coll=%o; model=%s (%o), options=%o', this, model.cid, model, options );
1070
- if ( !( model instanceof Backbone.Model ) || !( this.get( model ) || this.getByCid( model ) ) ) {
1071
- model = _add.call( this, model, options );
1167
+ var add = Backbone.Collection.prototype.add;
1168
+ Backbone.Collection.prototype.add = function( models, options ) {
1169
+ options || (options = {});
1170
+ if (!_.isArray( models ) ) {
1171
+ models = [ models ];
1072
1172
  }
1073
- this.trigger('relational:add', model, this, options);
1173
+
1174
+ //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
1175
+ _.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;
1182
+ }
1183
+ else {
1184
+ model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
1185
+ }
1186
+ }
1187
+
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 );
1074
1193
 
1075
- return model;
1194
+ return this;
1076
1195
  };
1077
1196
 
1078
1197
  /**
1079
- * Override 'Backbone.Collection._remove' to trigger 'relation:remove'.
1198
+ * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
1080
1199
  */
1081
- var _remove = Backbone.Collection.prototype._remove;
1082
- Backbone.Collection.prototype._remove = function( model, options ) {
1083
- //console.debug('calling _remove on coll=%o; model=%s (%o), options=%o', this, model.cid, model, options );
1084
- model = _remove.call( this, model, options );
1085
- this.trigger('relational:remove', model, this, options);
1200
+ var remove = Backbone.Collection.prototype.remove;
1201
+ Backbone.Collection.prototype.remove = function( models, options ) {
1202
+ options || (options = {});
1203
+ if (!_.isArray( models ) ) {
1204
+ models = [ models ];
1205
+ }
1206
+
1207
+ //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
1208
+ _.each( models, function( model ) {
1209
+ model = this.getByCid( model ) || this.get( model );
1210
+
1211
+ if ( model instanceof Backbone.Model ) {
1212
+ remove.call( this, model, options );
1213
+ this.trigger('relational:remove', model, this, options);
1214
+ }
1215
+ }, this );
1086
1216
 
1087
- return model;
1217
+ return this;
1218
+ };
1219
+
1220
+ /**
1221
+ * Override 'Backbone.Collection.reset' to trigger 'relational:reset'.
1222
+ */
1223
+ var reset = Backbone.Collection.prototype.reset;
1224
+ Backbone.Collection.prototype.reset = function( models, options ) {
1225
+ reset.call( this, models, options );
1226
+ this.trigger( 'relational:reset', models, options );
1227
+
1228
+ return this;
1088
1229
  };
1089
1230
 
1090
1231
  /**
@@ -1105,4 +1246,36 @@
1105
1246
 
1106
1247
  return this;
1107
1248
  };
1108
- })();
1249
+
1250
+ // Override .extend() to check for reverseRelations to initialize.
1251
+ Backbone.RelationalModel.extend = function( protoProps, classProps ) {
1252
+ 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
+ });
1277
+
1278
+ return child;
1279
+ };
1280
+
1281
+ })();
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backbone-relational-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -56,7 +56,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
56
  version: '0'
57
57
  segments:
58
58
  - 0
59
- hash: 920221681917958546
59
+ hash: -2906483573109266613
60
60
  required_rubygems_version: !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
@@ -65,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
65
  version: '0'
66
66
  segments:
67
67
  - 0
68
- hash: 920221681917958546
68
+ hash: -2906483573109266613
69
69
  requirements: []
70
70
  rubyforge_project:
71
71
  rubygems_version: 1.8.24