backbone-relational-rails 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -18,7 +18,7 @@ Add the following directive to your Javascript manifest file (application.js):
18
18
 
19
19
  ## Versioning
20
20
 
21
- backbone-relational-rails 0.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