frontend 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1108 @@
1
+ /**
2
+ * Backbone-relational.js 0.4.0
3
+ * (c) 2011 Paul Uithol
4
+ *
5
+ * Backbone-relational may be freely distributed under the MIT license.
6
+ * For details and documentation: https://github.com/PaulUithol/Backbone-relational.
7
+ * Depends on (as in, compeletely useless without) Backbone: https://github.com/documentcloud/backbone.
8
+ */
9
+ (function(undefined) {
10
+
11
+ /**
12
+ * CommonJS shim
13
+ **/
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;
22
+ }
23
+
24
+ Backbone.Relational = {};
25
+
26
+ /**
27
+ * Semaphore mixin; can be used as both binary and counting.
28
+ **/
29
+ Backbone.Semaphore = {
30
+ _permitsAvailable: null,
31
+ _permitsUsed: 0,
32
+
33
+ acquire: function() {
34
+ if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {
35
+ throw new Error('Max permits acquired');
36
+ }
37
+ else {
38
+ this._permitsUsed++;
39
+ }
40
+ },
41
+
42
+ release: function() {
43
+ if ( this._permitsUsed === 0 ) {
44
+ throw new Error('All permits released');
45
+ }
46
+ else {
47
+ this._permitsUsed--;
48
+ }
49
+ },
50
+
51
+ isLocked: function() {
52
+ return this._permitsUsed > 0;
53
+ },
54
+
55
+ setAvailablePermits: function( amount ) {
56
+ if ( this._permitsUsed > amount ) {
57
+ throw new Error('Available permits cannot be less than used permits');
58
+ }
59
+ this._permitsAvailable = amount;
60
+ }
61
+ };
62
+
63
+ /**
64
+ * A BlockingQueue that accumulates items while blocked (via 'block'),
65
+ * and processes them when unblocked (via 'unblock').
66
+ * Process can also be called manually (via 'process').
67
+ */
68
+ Backbone.BlockingQueue = function() {
69
+ this._queue = [];
70
+ };
71
+ _.extend( Backbone.BlockingQueue.prototype, Backbone.Semaphore, {
72
+ _queue: null,
73
+
74
+ add: function( func ) {
75
+ if ( this.isBlocked() ) {
76
+ this._queue.push( func );
77
+ }
78
+ else {
79
+ func();
80
+ }
81
+ },
82
+
83
+ process: function() {
84
+ while ( this._queue && this._queue.length ) {
85
+ this._queue.shift()();
86
+ }
87
+ },
88
+
89
+ block: function() {
90
+ this.acquire();
91
+ },
92
+
93
+ unblock: function() {
94
+ this.release();
95
+ if ( !this.isBlocked() ) {
96
+ this.process();
97
+ }
98
+ },
99
+
100
+ isBlocked: function() {
101
+ return this.isLocked();
102
+ }
103
+ });
104
+ /**
105
+ * Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'update:<key>')
106
+ * until the top-level object is fully initialized (see 'Backbone.RelationalModel').
107
+ */
108
+ Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
109
+
110
+ /**
111
+ * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
112
+ * Handles lookup for relations.
113
+ */
114
+ Backbone.Store = function() {
115
+ this._collections = [];
116
+ this._reverseRelations = [];
117
+ };
118
+ _.extend( Backbone.Store.prototype, Backbone.Events, {
119
+ _collections: null,
120
+ _reverseRelations: null,
121
+
122
+ /**
123
+ * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
124
+ * existing instances of 'model' in the store as well.
125
+ * @param {object} relation; required properties:
126
+ * - model, type, key and relatedModel
127
+ */
128
+ addReverseRelation: function( relation ) {
129
+ var exists = _.any( this._reverseRelations, function( rel ) {
130
+ return _.all( relation, function( val, key ) {
131
+ return val === rel[ key ];
132
+ });
133
+ });
134
+
135
+ if ( !exists && relation.model && relation.type ) {
136
+ this._reverseRelations.push( relation );
137
+
138
+ if ( !relation.model.prototype.relations ) {
139
+ relation.model.prototype.relations = [];
140
+ }
141
+ relation.model.prototype.relations.push( relation );
142
+
143
+ this.retroFitRelation( relation );
144
+ }
145
+ },
146
+
147
+ /**
148
+ * Add a 'relation' to all existing instances of 'relation.model' in the store
149
+ */
150
+ retroFitRelation: function( relation ) {
151
+ var coll = this.getCollection( relation.model );
152
+ coll.each( function( model ) {
153
+ var rel = new relation.type( model, relation );
154
+ }, this);
155
+ },
156
+
157
+ /**
158
+ * Find the Store's collection for a certain type of model.
159
+ * @param model {Backbone.RelationalModel}
160
+ * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
161
+ */
162
+ getCollection: function( model ) {
163
+ var coll = _.detect( this._collections, function( c ) {
164
+ // 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;
166
+ });
167
+
168
+ if ( !coll ) {
169
+ coll = this._createCollection( model );
170
+ }
171
+
172
+ return coll;
173
+ },
174
+
175
+ /**
176
+ * Find a type on the global object by name. Splits name on dots.
177
+ * @param {string} name
178
+ */
179
+ getObjectByName: function( name ) {
180
+ var type = _.reduce( name.split('.'), function( memo, val ) {
181
+ return memo[ val ];
182
+ }, exports);
183
+ return type !== exports ? type: null;
184
+ },
185
+
186
+ _createCollection: function( type ) {
187
+ var coll;
188
+
189
+ // If 'type' is an instance, take it's constructor
190
+ if ( type instanceof Backbone.RelationalModel ) {
191
+ type = type.constructor;
192
+ }
193
+
194
+ // Type should inherit from Backbone.RelationalModel.
195
+ if ( type.prototype instanceof Backbone.RelationalModel.prototype.constructor ) {
196
+ coll = new Backbone.Collection();
197
+ coll.model = type;
198
+
199
+ this._collections.push( coll );
200
+ }
201
+
202
+ return coll;
203
+ },
204
+
205
+ find: function( type, id ) {
206
+ var coll = this.getCollection( type );
207
+ return coll && coll.get( id );
208
+ },
209
+
210
+ /**
211
+ * Add a 'model' to it's appropriate collection. Retain the original contents of 'model.collection'.
212
+ */
213
+ register: function( model ) {
214
+ var modelColl = model.collection;
215
+ var coll = this.getCollection( model );
216
+ coll && coll._add( model );
217
+ model.collection = modelColl;
218
+ },
219
+
220
+ /**
221
+ * Explicitly update a model's id in it's store collection
222
+ */
223
+ update: function( model ) {
224
+ var coll = this.getCollection( model );
225
+ coll._onModelEvent( 'change:' + model.idAttribute, model, coll );
226
+ },
227
+
228
+ /**
229
+ * Remove a 'model' from the store.
230
+ */
231
+ unregister: function( model ) {
232
+ var coll = this.getCollection( model );
233
+ coll && coll.remove( model );
234
+ }
235
+ });
236
+ Backbone.Relational.store = new Backbone.Store();
237
+
238
+ /**
239
+ * The main Relation class, from which 'HasOne' and 'HasMany' inherit.
240
+ * @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'
251
+ */
252
+ Backbone.Relation = function( instance, options ) {
253
+ this.instance = instance;
254
+
255
+ // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
256
+ options = ( typeof options === 'object' && options ) || {};
257
+ this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
258
+ this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
259
+ Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
260
+ this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
261
+
262
+ this.key = this.options.key;
263
+ this.keyContents = this.instance.get( this.key );
264
+
265
+ // 'exports' should be the global object where 'relatedModel' can be found on if given as a string.
266
+ this.relatedModel = this.options.relatedModel;
267
+ if ( _.isString( this.relatedModel ) ) {
268
+ this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel );
269
+ }
270
+
271
+ if ( !this.checkPreconditions() ) {
272
+ return false;
273
+ }
274
+
275
+ // Add this Relation to instance._relations
276
+ this.instance._relations.push( this );
277
+
278
+ // Add the reverse relation on 'relatedModel' to the store's reverseRelations
279
+ if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
280
+ Backbone.Relational.store.addReverseRelation( _.defaults( {
281
+ isAutoRelation: true,
282
+ model: this.relatedModel,
283
+ relatedModel: this.instance.constructor,
284
+ reverseRelation: this.options // current relation is the 'reverseRelation' for it's own reverseRelation
285
+ },
286
+ this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
287
+ ) );
288
+ }
289
+
290
+ this.initialize();
291
+
292
+ _.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 );
301
+ };
302
+ // Fix inheritance :\
303
+ Backbone.Relation.extend = Backbone.Model.extend;
304
+ // Set up all inheritable **Backbone.Relation** properties and methods.
305
+ _.extend( Backbone.Relation.prototype, Backbone.Events, Backbone.Semaphore, {
306
+ options: {
307
+ createModels: true,
308
+ includeInJSON: true,
309
+ isAutoRelation: false
310
+ },
311
+
312
+ instance: null,
313
+ key: null,
314
+ keyContents: null,
315
+ relatedModel: null,
316
+ reverseRelation: null,
317
+ related: null,
318
+
319
+ _relatedModelAdded: function( model, coll, options ) {
320
+ // Allow 'model' to set up it's relations, before calling 'tryAddRelated'
321
+ // (which can result in a call to 'addRelated' on a relation of 'model')
322
+ var dit = this;
323
+ model.queue( function() {
324
+ dit.tryAddRelated( model, options );
325
+ });
326
+ },
327
+
328
+ _relatedModelRemoved: function( model, coll, options ) {
329
+ this.removeRelated( model, options );
330
+ },
331
+
332
+ _modelRemovedFromCollection: function( model ) {
333
+ if ( model === this.instance ) {
334
+ this.destroy();
335
+ }
336
+ },
337
+
338
+ /**
339
+ * Check several pre-conditions.
340
+ * @return {bool} True if pre-conditions are satisfied, false if they're not.
341
+ */
342
+ 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 );
346
+ return false;
347
+ }
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 );
351
+ return false;
352
+ }
353
+ // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
354
+ if ( !( rm.prototype instanceof Backbone.RelationalModel.prototype.constructor ) ) {
355
+ console && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
356
+ return false;
357
+ }
358
+ // Check if this is not a HasMany, and the reverse relation is HasMany as well
359
+ 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 );
361
+ return false;
362
+ }
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',
373
+ this, i, k, rm, this.reverseRelation.key );
374
+ return false;
375
+ }
376
+ }
377
+ return true;
378
+ },
379
+
380
+ setRelated: function( related, options ) {
381
+ this.related = related;
382
+ var value = {};
383
+ value[ this.key ] = related;
384
+ this.instance.acquire();
385
+ this.instance.set( value, _.defaults( options || {}, { silent: true } ) );
386
+ this.instance.release();
387
+ },
388
+
389
+ createModel: function( item ) {
390
+ if ( this.options.createModels && typeof( item ) === 'object' ) {
391
+ return new this.relatedModel( item );
392
+ }
393
+ },
394
+
395
+ /**
396
+ * Determine if a relation (on a different RelationalModel) is the reverse
397
+ * relation of the current one.
398
+ */
399
+ _isReverseRelation: function( relation ) {
400
+ if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key
401
+ && this.key === relation.reverseRelation.key ) {
402
+ return true;
403
+ }
404
+ return false;
405
+ },
406
+
407
+ /**
408
+ * 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.
411
+ */
412
+ getReverseRelations: function( model ) {
413
+ var reverseRelations = [];
414
+ // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
415
+ var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
416
+ _.each( models , function( related ) {
417
+ _.each( related.getRelations(), function( relation ) {
418
+ if ( this._isReverseRelation( relation ) ) {
419
+ reverseRelations.push( relation );
420
+ }
421
+ }, this );
422
+ }, this );
423
+
424
+ return reverseRelations;
425
+ },
426
+
427
+ /**
428
+ * Rename options.silent, so add/remove events propagate properly.
429
+ * (for example in HasMany, from 'addRelated'->'handleAddition')
430
+ */
431
+ sanitizeOptions: function( options ) {
432
+ options || ( options = {} );
433
+ if ( options.silent ) {
434
+ options = _.extend( {}, options, { silentChange: true } );
435
+ delete options.silent;
436
+ }
437
+ return options;
438
+ },
439
+
440
+ // Cleanup. Get reverse relation, call removeRelated on each.
441
+ destroy: function() {
442
+ Backbone.Relational.store.getCollection( this.instance )
443
+ .unbind( 'relational:remove', this._modelRemovedFromCollection );
444
+
445
+ Backbone.Relational.store.getCollection( this.relatedModel )
446
+ .unbind( 'relational:add', this._relatedModelAdded )
447
+ .unbind( 'relational:remove', this._relatedModelRemoved );
448
+
449
+ _.each( this.getReverseRelations(), function( relation ) {
450
+ relation.removeRelated( this.instance );
451
+ }, this );
452
+ }
453
+ });
454
+
455
+ Backbone.HasOne = Backbone.Relation.extend({
456
+ options: {
457
+ reverseRelation: { type: 'HasMany' }
458
+ },
459
+
460
+ initialize: function() {
461
+ _.bindAll( this, 'onChange' );
462
+ this.instance.bind( 'relational:change:' + this.key, this.onChange );
463
+
464
+ var model = this.findRelated();
465
+ this.setRelated( model );
466
+
467
+ // Notify new 'related' object of the new relation.
468
+ var dit = this;
469
+ _.each( dit.getReverseRelations(), function( relation ) {
470
+ relation.addRelated( dit.instance );
471
+ } );
472
+ },
473
+
474
+ findRelated: function() {
475
+ var item = this.keyContents;
476
+ var model = null;
477
+
478
+ if ( item instanceof this.relatedModel ) {
479
+ model = item;
480
+ }
481
+ else if ( item && ( _.isString( item ) || _.isNumber( item ) || typeof( item ) === 'object' ) ) {
482
+ // Try to find an instance of the appropriate 'relatedModel' in the store, or create it
483
+ var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
484
+ model = Backbone.Relational.store.find( this.relatedModel, id ) || this.createModel( item );
485
+ }
486
+
487
+ return model;
488
+ },
489
+
490
+ /**
491
+ * If the key is changed, notify old & new reverse relations and initialize the new relation
492
+ */
493
+ onChange: function( model, attr, options ) {
494
+ // Don't accept recursive calls to onChange (like onChange->findRelated->createModel->initializeRelations->addRelated->onChange)
495
+ if ( this.isLocked() ) {
496
+ return;
497
+ }
498
+ this.acquire();
499
+ options = this.sanitizeOptions( options );
500
+
501
+ // 'options._related' is set by 'addRelated'/'removeRelated'. If it is set, the change
502
+ // is the result of a call from a relation. If it's not, the change is the result of
503
+ // a 'set' call on this.instance.
504
+ var changed = _.isUndefined( options._related );
505
+ var oldRelated = changed ? this.related : options._related;
506
+
507
+ if ( changed ) {
508
+ this.keyContents = attr;
509
+
510
+ // Set new 'related'
511
+ if ( attr instanceof this.relatedModel ) {
512
+ this.related = attr;
513
+ }
514
+ else if ( attr ) {
515
+ var related = this.findRelated();
516
+ this.setRelated( related );
517
+ }
518
+ else {
519
+ this.setRelated( null );
520
+ }
521
+ }
522
+
523
+ // Notify old 'related' object of the terminated relation
524
+ if ( oldRelated && this.related !== oldRelated ) {
525
+ _.each( this.getReverseRelations( oldRelated ), function( relation ) {
526
+ relation.removeRelated( this.instance, options );
527
+ }, this );
528
+ }
529
+
530
+ // Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated;
531
+ // that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'.
532
+ // In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet.
533
+ _.each( this.getReverseRelations(), function( relation ) {
534
+ relation.addRelated( this.instance, options );
535
+ }, this);
536
+
537
+ // Fire the 'update:<key>' event if 'related' was updated
538
+ if ( !options.silentChange && this.related !== oldRelated ) {
539
+ var dit = this;
540
+ Backbone.Relational.eventQueue.add( function() {
541
+ dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
542
+ });
543
+ }
544
+ this.release();
545
+ },
546
+
547
+ /**
548
+ * If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents'
549
+ */
550
+ tryAddRelated: function( model, options ) {
551
+ if ( this.related ) {
552
+ return;
553
+ }
554
+ options = this.sanitizeOptions( options );
555
+
556
+ var item = this.keyContents;
557
+ if ( item && ( _.isString( item ) || _.isNumber( item ) || typeof( item ) === 'object' ) ) {
558
+ var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
559
+ if ( model.id === id ) {
560
+ this.addRelated( model, options );
561
+ }
562
+ }
563
+ },
564
+
565
+ addRelated: function( model, options ) {
566
+ if ( model !== this.related ) {
567
+ var oldRelated = this.related || null;
568
+ this.setRelated( model );
569
+ this.onChange( this.instance, model, { _related: oldRelated } );
570
+ }
571
+ },
572
+
573
+ removeRelated: function( model, options ) {
574
+ if ( !this.related ) {
575
+ return;
576
+ }
577
+
578
+ if ( model === this.related ) {
579
+ var oldRelated = this.related || null;
580
+ this.setRelated( null );
581
+ this.onChange( this.instance, model, { _related: oldRelated } );
582
+ }
583
+ }
584
+ });
585
+
586
+ Backbone.HasMany = Backbone.Relation.extend({
587
+ collectionType: null,
588
+
589
+ options: {
590
+ reverseRelation: { type: 'HasOne' },
591
+ collectionType: Backbone.Collection
592
+ },
593
+
594
+ initialize: function() {
595
+ _.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval' );
596
+ this.instance.bind( 'relational:change:' + this.key, this.onChange );
597
+
598
+ // Handle a custom 'collectionType'
599
+ this.collectionType = this.options.collectionType;
600
+ if ( _( this.collectionType ).isString() ) {
601
+ this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
602
+ }
603
+ if ( !this.collectionType.prototype instanceof Backbone.Collection.prototype.constructor ){
604
+ throw new Error( 'collectionType must inherit from Backbone.Collection' );
605
+ }
606
+
607
+ this.setRelated( this.prepareCollection( new this.collectionType() ) );
608
+ this.findRelated();
609
+ },
610
+
611
+ prepareCollection: function( collection ) {
612
+ if ( this.related ) {
613
+ this.related.unbind( 'relational:add', this.handleAddition ).unbind('relational:remove', this.handleRemoval );
614
+ }
615
+
616
+ collection.reset();
617
+ collection.model = this.relatedModel;
618
+ collection.bind( 'relational:add', this.handleAddition ).bind('relational:remove', this.handleRemoval );
619
+ return collection;
620
+ },
621
+
622
+ findRelated: function() {
623
+ if ( this.keyContents && _.isArray( this.keyContents ) ) {
624
+ // Try to find instances of the appropriate 'relatedModel' in the store
625
+ _.each( this.keyContents, function( item ) {
626
+ 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 );
628
+
629
+ if ( model && !this.related.getByCid( model ) && !this.related.get( model ) ) {
630
+ this.related._add( model );
631
+ }
632
+ }, this);
633
+ }
634
+ },
635
+
636
+ /**
637
+ * If the key is changed, notify old & new reverse relations and initialize the new relation
638
+ */
639
+ onChange: function( model, attr, options ) {
640
+ options = this.sanitizeOptions( options );
641
+ this.keyContents = attr;
642
+
643
+ // Notify old 'related' object of the terminated relation
644
+ _.each( this.getReverseRelations(), function( relation ) {
645
+ relation.removeRelated( this.instance, options );
646
+ }, this );
647
+
648
+ // Replace 'this.related' by 'attr' if it is a Backbone.Collection
649
+ if ( attr instanceof Backbone.Collection ) {
650
+ this.prepareCollection( attr );
651
+ this.related = attr;
652
+ }
653
+ // Otherwise, 'attr' should be an array of related object ids
654
+ else {
655
+ // Re-use the current 'this.related' if it is a Backbone.Collection
656
+ var coll = this.related instanceof Backbone.Collection ? this.related : new this.collectionType();
657
+ this.setRelated( this.prepareCollection( coll ) );
658
+ this.findRelated();
659
+ }
660
+
661
+ // Notify new 'related' object of the new relation
662
+ _.each( this.getReverseRelations(), function( relation ) {
663
+ relation.addRelated( this.instance, options );
664
+ }, this );
665
+
666
+ var dit = this;
667
+ Backbone.Relational.eventQueue.add( function() {
668
+ !options.silentChange && dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
669
+ });
670
+ },
671
+
672
+ tryAddRelated: function( model, options ) {
673
+ options = this.sanitizeOptions( options );
674
+ if ( !this.related.getByCid( model ) && !this.related.get( model ) ) {
675
+ // Check if this new model was specified in 'this.keyContents'
676
+ var item = _.any( this.keyContents, function( item ) {
677
+ var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
678
+ return id && id === model.id;
679
+ }, this );
680
+
681
+ if ( item ) {
682
+ this.related._add( model, options );
683
+ }
684
+ }
685
+ },
686
+
687
+ /**
688
+ * When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
689
+ * (should be 'HasOne', must set 'this.instance' as their related).
690
+ */
691
+ handleAddition: function( model, coll, options ) {
692
+ //console.debug('handleAddition called; args=%o', arguments);
693
+ options = this.sanitizeOptions( options );
694
+ var dit = this;
695
+
696
+ _.each( this.getReverseRelations( model ), function( relation ) {
697
+ relation.addRelated( this.instance, options );
698
+ }, this );
699
+
700
+ // Only trigger 'add' once the newly added model is initialized (so, has it's relations set up)
701
+ Backbone.Relational.eventQueue.add( function() {
702
+ !options.silentChange && dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
703
+ });
704
+ },
705
+
706
+ /**
707
+ * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations.
708
+ * (should be 'HasOne', which should be nullified)
709
+ */
710
+ handleRemoval: function( model, coll, options ) {
711
+ //console.debug('handleRemoval called; args=%o', arguments);
712
+ options = this.sanitizeOptions( options );
713
+
714
+ _.each( this.getReverseRelations( model ), function( relation ) {
715
+ relation.removeRelated( this.instance, options );
716
+ }, this );
717
+
718
+ var dit = this;
719
+ Backbone.Relational.eventQueue.add( function() {
720
+ !options.silentChange && dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
721
+ });
722
+ },
723
+
724
+ addRelated: function( model, options ) {
725
+ var dit = this;
726
+ model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice
727
+ if ( dit.related && !dit.related.getByCid( model ) && !dit.related.get( model ) ) {
728
+ dit.related._add( model, options );
729
+ }
730
+ });
731
+ },
732
+
733
+ removeRelated: function( model, options ) {
734
+ if ( this.related.getByCid( model ) || this.related.get( model ) ) {
735
+ this.related.remove( model, options );
736
+ }
737
+ }
738
+ });
739
+
740
+ /**
741
+ * New events:
742
+ * - 'add:<key>' (model, related collection, options)
743
+ * - 'remove:<key>' (model, related collection, options)
744
+ * - 'update:<key>' (model, related model or collection, options)
745
+ */
746
+ Backbone.RelationalModel = Backbone.Model.extend({
747
+ relations: null, // Relation descriptions on the prototype
748
+ _relations: null, // Relation instances
749
+ _isInitialized: false,
750
+ _deferProcessing: false,
751
+ _queue: null,
752
+
753
+ constructor: function( attributes, options ) {
754
+ // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
755
+ // 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"
757
+ // (by creating a model from properties, having the model add itself to the collection via one of
758
+ // it's relations, then trying to add it to the collection).
759
+ // b) Trigger 'HasMany' collection events only after the model is really fully set up.
760
+ // Example that triggers both a and b: "p.get('jobs').add( { company: c, person: p } )".
761
+ var dit = this;
762
+ if ( options && options.collection ) {
763
+ this._deferProcessing = true;
764
+
765
+ var processQueue = function( model, coll ) {
766
+ if ( model === dit ) {
767
+ dit._deferProcessing = false;
768
+ dit.processQueue();
769
+ options.collection.unbind( 'relational:add', processQueue );
770
+ }
771
+ };
772
+ options.collection.bind( 'relational:add', processQueue );
773
+
774
+ // So we do process the queue eventually, regardless of whether this model really gets added to 'options.collection'.
775
+ _.defer( function() {
776
+ processQueue( dit );
777
+ });
778
+ }
779
+
780
+ this._queue = new Backbone.BlockingQueue();
781
+ this._queue.block();
782
+ Backbone.Relational.eventQueue.block();
783
+
784
+ Backbone.Model.prototype.constructor.apply( this, arguments );
785
+
786
+ // Try to run the global queue holding external events
787
+ Backbone.Relational.eventQueue.unblock();
788
+ },
789
+
790
+ /**
791
+ * Override 'trigger' to queue 'change' and 'change:*' events
792
+ */
793
+ trigger: function( eventName ) {
794
+ if ( eventName.length > 5 && 'change' === eventName.substr( 0, 6 ) ) {
795
+ var dit = this, args = arguments;
796
+ Backbone.Relational.eventQueue.add( function() {
797
+ Backbone.Model.prototype.trigger.apply( dit, args );
798
+ });
799
+ }
800
+ else {
801
+ Backbone.Model.prototype.trigger.apply( this, arguments );
802
+ }
803
+
804
+ return this;
805
+ },
806
+
807
+ /**
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!
810
+ */
811
+ initializeRelations: function() {
812
+ this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
813
+ this._relations = [];
814
+
815
+ _.each( this.relations, function( rel ) {
816
+ var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
817
+ if ( type && type.prototype instanceof Backbone.Relation.prototype.constructor ) {
818
+ new type( this, rel ); // Also pushes the new Relation into _relations
819
+ }
820
+ else {
821
+ console && console.warn( 'Relation=%o; missing or invalid type!', rel );
822
+ }
823
+ }, this );
824
+
825
+ this._isInitialized = true;
826
+ this.release();
827
+ this.processQueue();
828
+ },
829
+
830
+ /**
831
+ * When new values are set, notify this model's relations (also if options.silent is set).
832
+ * (Relation.setRelated locks this model before calling 'set' on it to prevent loops)
833
+ */
834
+ updateRelations: function( options ) {
835
+ if( this._isInitialized && !this.isLocked() ) {
836
+ _.each( this._relations, function( rel ) {
837
+ var val = this.attributes[ rel.key ];
838
+ if ( rel.related !== val ) {
839
+ this.trigger('relational:change:' + rel.key, this, val, options || {} );
840
+ }
841
+ }, this );
842
+ }
843
+ },
844
+
845
+ /**
846
+ * Either add to the queue (if we're not initialized yet), or execute right away.
847
+ */
848
+ queue: function( func ) {
849
+ this._queue.add( func );
850
+ },
851
+
852
+ /**
853
+ * Process _queue
854
+ */
855
+ processQueue: function() {
856
+ if ( this._isInitialized && !this._deferProcessing && this._queue.isBlocked() ) {
857
+ this._queue.unblock();
858
+ }
859
+ },
860
+
861
+ /**
862
+ * Get a specific relation.
863
+ * @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.
865
+ */
866
+ getRelation: function( key ) {
867
+ return _.detect( this._relations, function( rel ) {
868
+ if ( rel.key === key ) {
869
+ return true;
870
+ }
871
+ }, this );
872
+ },
873
+
874
+ /**
875
+ * Get all of the created relations.
876
+ * @return {array of Backbone.Relation}
877
+ */
878
+ getRelations: function() {
879
+ return this._relations;
880
+ },
881
+
882
+ /**
883
+ * Retrieve related objects.
884
+ * @param key {string} The relation key to fetch models for.
885
+ * @param options {object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
886
+ * @return {xhr} An array or request objects
887
+ */
888
+ fetchRelated: function( key, options ) {
889
+ options || ( options = {} );
890
+ var rel = this.getRelation( key ),
891
+ keyContents = rel && rel.keyContents,
892
+ 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 );
896
+
897
+ if ( toFetch && toFetch.length ) {
898
+ // Create a model for each entry in 'keyContents' that is to be fetched
899
+ 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 );
910
+
911
+ // Try if the 'collection' can provide a url to fetch a set of models in one request.
912
+ if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) {
913
+ var setUrl = rel.related.url( models );
914
+ }
915
+
916
+ // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls.
917
+ // To make sure it can, test if the url we got by supplying a list of models to fetch is different from
918
+ // the one supplied for the default fetch action (without args to 'url').
919
+ if ( setUrl && setUrl !== rel.related.url() ) {
920
+ var opts = _.defaults( {
921
+ error: function() {
922
+ var args = arguments;
923
+ _.each( models, function( model ) {
924
+ model.destroy();
925
+ options.error && options.error.apply( model, args );
926
+ })
927
+ },
928
+ url: setUrl
929
+ },
930
+ options,
931
+ { add: true }
932
+ );
933
+
934
+ var requests = [ rel.related.fetch( opts ) ];
935
+ }
936
+ else {
937
+ var requests = _.map( models, function( model ) {
938
+ var opts = _.defaults( {
939
+ 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 );
948
+ }
949
+ }
950
+
951
+ return _.isUndefined( requests ) ? [] : requests;
952
+ },
953
+
954
+ set: function( attributes, options ) {
955
+ Backbone.Relational.eventQueue.block();
956
+
957
+ var result = Backbone.Model.prototype.set.apply( this, arguments );
958
+
959
+ // 'set' is called quite late in 'Backbone.Model.prototype.constructor', but before 'initialize'.
960
+ // Ideal place to set up relations :)
961
+ if ( !this._isInitialized && !this.isLocked() ) {
962
+ Backbone.Relational.store.register( this );
963
+ this.initializeRelations();
964
+ }
965
+ // Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
966
+ else if ( attributes && this.idAttribute in attributes ) {
967
+ Backbone.Relational.store.update( this );
968
+ }
969
+
970
+ this.updateRelations( options );
971
+
972
+ // Try to run the global queue holding external events
973
+ Backbone.Relational.eventQueue.unblock();
974
+
975
+ return result;
976
+ },
977
+
978
+ unset: function( attributes, options ) {
979
+ Backbone.Relational.eventQueue.block();
980
+
981
+ var result = Backbone.Model.prototype.unset.apply( this, arguments );
982
+ this.updateRelations( options );
983
+
984
+ // Try to run the global queue holding external events
985
+ Backbone.Relational.eventQueue.unblock();
986
+
987
+ return result;
988
+ },
989
+
990
+ clear: function( options ) {
991
+ Backbone.Relational.eventQueue.block();
992
+
993
+ var result = Backbone.Model.prototype.clear.apply( this, arguments );
994
+ this.updateRelations( options );
995
+
996
+ // Try to run the global queue holding external events
997
+ Backbone.Relational.eventQueue.unblock();
998
+
999
+ return result;
1000
+ },
1001
+
1002
+ /**
1003
+ * Override 'change', so the change will only execute after 'set' has finised (relations are updated),
1004
+ * and 'previousAttributes' will be available when the event is fired.
1005
+ */
1006
+ change: function( options ) {
1007
+ var dit = this;
1008
+ Backbone.Relational.eventQueue.add( function() {
1009
+ Backbone.Model.prototype.change.apply( dit, arguments );
1010
+ });
1011
+ },
1012
+
1013
+ destroy: function( options ) {
1014
+ Backbone.Relational.store.unregister( this );
1015
+ return Backbone.Model.prototype.destroy.call( this, options );
1016
+ },
1017
+
1018
+ /**
1019
+ * Convert relations to JSON, omits them when required
1020
+ */
1021
+ toJSON: function() {
1022
+ // If this Model has already been fully serialized in this branch once, return to avoid loops
1023
+ if ( this.isLocked() ) {
1024
+ return this.id;
1025
+ }
1026
+
1027
+ var json = Backbone.Model.prototype.toJSON.call( this );
1028
+
1029
+ _.each( this._relations, function( rel ) {
1030
+ var value = json[ rel.key ];
1031
+
1032
+ if ( rel.options.includeInJSON === true && value && _.isFunction( value.toJSON ) ) {
1033
+ this.acquire();
1034
+ json[ rel.key ] = value.toJSON();
1035
+ this.release();
1036
+ }
1037
+ else if ( _.isString( rel.options.includeInJSON ) ) {
1038
+ if ( value instanceof Backbone.Collection ) {
1039
+ json[ rel.key ] = value.pluck( rel.options.includeInJSON );
1040
+ }
1041
+ else if ( value instanceof Backbone.Model ) {
1042
+ json[ rel.key ] = value.get( rel.options.includeInJSON );
1043
+ }
1044
+ }
1045
+ else {
1046
+ delete json[ rel.key ];
1047
+ }
1048
+ }, this );
1049
+
1050
+ return json;
1051
+ }
1052
+ });
1053
+ _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
1054
+
1055
+ /**
1056
+ * Override Backbone.Collection._add, so objects fetched from the server multiple times will
1057
+ * update the existing Model. Also, trigger 'relation:add'.
1058
+ */
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 );
1072
+ }
1073
+ this.trigger('relational:add', model, this, options);
1074
+
1075
+ return model;
1076
+ };
1077
+
1078
+ /**
1079
+ * Override 'Backbone.Collection._remove' to trigger 'relation:remove'.
1080
+ */
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);
1086
+
1087
+ return model;
1088
+ };
1089
+
1090
+ /**
1091
+ * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
1092
+ * are ready.
1093
+ */
1094
+ var _trigger = Backbone.Collection.prototype.trigger;
1095
+ Backbone.Collection.prototype.trigger = function( eventName ) {
1096
+ if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) {
1097
+ var dit = this, args = arguments;
1098
+ Backbone.Relational.eventQueue.add( function() {
1099
+ _trigger.apply( dit, args );
1100
+ });
1101
+ }
1102
+ else {
1103
+ _trigger.apply( this, arguments );
1104
+ }
1105
+
1106
+ return this;
1107
+ };
1108
+ })();