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 +1 -1
- data/lib/backbone-relational-rails/version.rb +1 -1
- data/vendor/assets/javascripts/backbone-relational.js +358 -185
- metadata +3 -3
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.
|
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,27 +1,30 @@
|
|
1
1
|
/**
|
2
|
-
* Backbone-relational.js 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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
var
|
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
|
126
|
-
*
|
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
|
-
|
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
|
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
|
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 {
|
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.
|
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
|
-
*
|
243
|
-
*
|
244
|
-
*
|
245
|
-
*
|
246
|
-
*
|
247
|
-
*
|
248
|
-
*
|
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
|
-
|
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
|
-
|
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
|
-
|
276
|
-
|
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.
|
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
|
-
|
294
|
-
|
295
|
-
.
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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 {
|
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 ( !
|
345
|
-
console && console.warn( 'Relation=%o; no
|
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 '
|
349
|
-
if ( !(
|
350
|
-
console && console.warn( 'Relation=%o;
|
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
|
-
|
364
|
-
if
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
-
|
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
|
-
|
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
|
410
|
-
*
|
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
|
-
|
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 )
|
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
|
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
|
-
|
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
|
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
|
-
|
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.
|
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.
|
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.
|
807
|
+
dit.related.add( model, options );
|
729
808
|
}
|
730
809
|
});
|
731
810
|
},
|
@@ -738,7 +817,8 @@
|
|
738
817
|
});
|
739
818
|
|
740
819
|
/**
|
741
|
-
*
|
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.
|
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
|
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
|
809
|
-
*
|
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
|
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
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
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 {
|
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 {
|
966
|
+
* @return {jQuery.when[]} An array of request objects
|
887
967
|
*/
|
888
968
|
fetchRelated: function( key, options ) {
|
889
969
|
options || ( options = {} );
|
890
|
-
var
|
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
|
-
|
894
|
-
|
895
|
-
|
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
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
}
|
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
|
-
|
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
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
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
|
-
|
1019
|
+
|
1020
|
+
requests = [ rel.related.fetch( opts ) ];
|
935
1021
|
}
|
936
1022
|
else {
|
937
|
-
|
938
|
-
|
1023
|
+
requests = _.map( models, function( model ) {
|
1024
|
+
var opts = _.defaults(
|
1025
|
+
{
|
939
1026
|
error: function() {
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
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
|
1038
|
+
return requests;
|
952
1039
|
},
|
953
1040
|
|
954
|
-
set: function(
|
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
|
-
|
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(
|
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
|
-
|
1014
|
-
|
1015
|
-
|
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.
|
1057
|
-
* update the existing Model. Also, trigger '
|
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
|
1060
|
-
Backbone.Collection.prototype.
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
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
|
-
|
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
|
1194
|
+
return this;
|
1076
1195
|
};
|
1077
1196
|
|
1078
1197
|
/**
|
1079
|
-
* Override 'Backbone.Collection.
|
1198
|
+
* Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
|
1080
1199
|
*/
|
1081
|
-
var
|
1082
|
-
Backbone.Collection.prototype.
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
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
|
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
|
+
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:
|
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:
|
68
|
+
hash: -2906483573109266613
|
69
69
|
requirements: []
|
70
70
|
rubyforge_project:
|
71
71
|
rubygems_version: 1.8.24
|