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