backbone-relational-rails 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +1 -1
- data/lib/backbone-relational-rails/version.rb +1 -1
- data/vendor/assets/javascripts/backbone-relational.js +619 -214
- metadata +3 -3
data/README.md
CHANGED
@@ -18,7 +18,7 @@ Add the following directive to your Javascript manifest file (application.js):
|
|
18
18
|
|
19
19
|
## Versioning
|
20
20
|
|
21
|
-
backbone-relational-rails 0.
|
21
|
+
backbone-relational-rails 0.6.0 == Backbone-relational 0.6.0
|
22
22
|
|
23
23
|
Every attempt is made to mirror the currently shipping Backbone-relational version number wherever possible.
|
24
24
|
The major, minor, and patch version numbers will always represent the Backbone-relational version. Should a gem
|
@@ -1,12 +1,14 @@
|
|
1
|
-
/**
|
2
|
-
* Backbone-relational.js 0.
|
1
|
+
/**
|
2
|
+
* Backbone-relational.js 0.6.0
|
3
3
|
* (c) 2011 Paul Uithol
|
4
4
|
*
|
5
|
-
* Backbone-relational may be freely distributed under the MIT license.
|
5
|
+
* Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.
|
6
6
|
* For details and documentation: https://github.com/PaulUithol/Backbone-relational.
|
7
|
-
* Depends on (
|
7
|
+
* Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
|
8
8
|
*/
|
9
9
|
( function( undefined ) {
|
10
|
+
"use strict";
|
11
|
+
|
10
12
|
/**
|
11
13
|
* CommonJS shim
|
12
14
|
**/
|
@@ -17,15 +19,15 @@
|
|
17
19
|
exports = module.exports = Backbone;
|
18
20
|
}
|
19
21
|
else {
|
20
|
-
|
22
|
+
_ = window._;
|
21
23
|
Backbone = window.Backbone;
|
22
24
|
exports = window;
|
23
25
|
}
|
24
|
-
|
26
|
+
|
25
27
|
Backbone.Relational = {
|
26
28
|
showWarnings: true
|
27
29
|
};
|
28
|
-
|
30
|
+
|
29
31
|
/**
|
30
32
|
* Semaphore mixin; can be used as both binary and counting.
|
31
33
|
**/
|
@@ -114,37 +116,87 @@
|
|
114
116
|
* Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
|
115
117
|
* Handles lookup for relations.
|
116
118
|
*/
|
117
|
-
Backbone.Store =
|
119
|
+
Backbone.Store = function() {
|
118
120
|
this._collections = [];
|
119
121
|
this._reverseRelations = [];
|
122
|
+
this._subModels = [];
|
123
|
+
this._modelScopes = [ exports ];
|
120
124
|
};
|
121
125
|
_.extend( Backbone.Store.prototype, Backbone.Events, {
|
122
|
-
|
123
|
-
|
126
|
+
addModelScope: function( scope ) {
|
127
|
+
this._modelScopes.push( scope );
|
128
|
+
},
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Add a set of subModelTypes to the store, that can be used to resolve the '_superModel'
|
132
|
+
* for a model later in 'setupSuperModel'.
|
133
|
+
*
|
134
|
+
* @param {Backbone.RelationalModel} subModelTypes
|
135
|
+
* @param {Backbone.RelationalModel} superModelType
|
136
|
+
*/
|
137
|
+
addSubModels: function( subModelTypes, superModelType ) {
|
138
|
+
this._subModels.push({
|
139
|
+
'superModelType': superModelType,
|
140
|
+
'subModels': subModelTypes
|
141
|
+
});
|
142
|
+
},
|
143
|
+
|
144
|
+
/**
|
145
|
+
* Check if the given modelType is registered as another model's subModel. If so, add it to the super model's
|
146
|
+
* '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'.
|
147
|
+
*
|
148
|
+
* @param {Backbone.RelationalModel} modelType
|
149
|
+
*/
|
150
|
+
setupSuperModel: function( modelType ) {
|
151
|
+
_.find( this._subModels, function( subModelDef ) {
|
152
|
+
return _.find( subModelDef.subModels, function( subModelTypeName, typeValue ) {
|
153
|
+
var subModelType = this.getObjectByName( subModelTypeName );
|
154
|
+
|
155
|
+
if ( modelType === subModelType ) {
|
156
|
+
// Set 'modelType' as a child of the found superModel
|
157
|
+
subModelDef.superModelType._subModels[ typeValue ] = modelType;
|
158
|
+
|
159
|
+
// Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'.
|
160
|
+
modelType._superModel = subModelDef.superModelType;
|
161
|
+
modelType._subModelTypeValue = typeValue;
|
162
|
+
modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute;
|
163
|
+
return true;
|
164
|
+
}
|
165
|
+
}, this );
|
166
|
+
}, this );
|
167
|
+
},
|
124
168
|
|
125
169
|
/**
|
126
170
|
* Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
|
127
171
|
* existing instances of 'model' in the store as well.
|
128
|
-
* @param {
|
172
|
+
* @param {Object} relation
|
129
173
|
* @param {Backbone.RelationalModel} relation.model
|
130
174
|
* @param {String} relation.type
|
131
175
|
* @param {String} relation.key
|
132
|
-
* @param {String|
|
176
|
+
* @param {String|Object} relation.relatedModel
|
133
177
|
*/
|
134
178
|
addReverseRelation: function( relation ) {
|
135
179
|
var exists = _.any( this._reverseRelations, function( rel ) {
|
136
|
-
|
137
|
-
|
180
|
+
return _.all( relation, function( val, key ) {
|
181
|
+
return val === rel[ key ];
|
182
|
+
});
|
138
183
|
});
|
139
|
-
});
|
140
184
|
|
141
185
|
if ( !exists && relation.model && relation.type ) {
|
142
186
|
this._reverseRelations.push( relation );
|
143
187
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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 );
|
148
200
|
|
149
201
|
this.retroFitRelation( relation );
|
150
202
|
}
|
@@ -152,11 +204,15 @@
|
|
152
204
|
|
153
205
|
/**
|
154
206
|
* Add a 'relation' to all existing instances of 'relation.model' in the store
|
155
|
-
* @param {
|
207
|
+
* @param {Object} relation
|
156
208
|
*/
|
157
209
|
retroFitRelation: function( relation ) {
|
158
210
|
var coll = this.getCollection( relation.model );
|
159
211
|
coll.each( function( model ) {
|
212
|
+
if ( !( model instanceof relation.model ) ) {
|
213
|
+
return;
|
214
|
+
}
|
215
|
+
|
160
216
|
new relation.type( model, relation );
|
161
217
|
}, this);
|
162
218
|
},
|
@@ -167,13 +223,21 @@
|
|
167
223
|
* @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
|
168
224
|
*/
|
169
225
|
getCollection: function( model ) {
|
170
|
-
|
171
|
-
|
172
|
-
|
226
|
+
if ( model instanceof Backbone.RelationalModel ) {
|
227
|
+
model = model.constructor;
|
228
|
+
}
|
229
|
+
|
230
|
+
var rootModel = model;
|
231
|
+
while ( rootModel._superModel ) {
|
232
|
+
rootModel = rootModel._superModel;
|
233
|
+
}
|
234
|
+
|
235
|
+
var coll = _.detect( this._collections, function( c ) {
|
236
|
+
return c.model === rootModel;
|
173
237
|
});
|
174
238
|
|
175
239
|
if ( !coll ) {
|
176
|
-
coll = this._createCollection(
|
240
|
+
coll = this._createCollection( rootModel );
|
177
241
|
}
|
178
242
|
|
179
243
|
return coll;
|
@@ -182,25 +246,35 @@
|
|
182
246
|
/**
|
183
247
|
* Find a type on the global object by name. Splits name on dots.
|
184
248
|
* @param {String} name
|
185
|
-
* @return {
|
249
|
+
* @return {Object}
|
186
250
|
*/
|
187
251
|
getObjectByName: function( name ) {
|
188
|
-
var
|
189
|
-
|
190
|
-
|
191
|
-
|
252
|
+
var parts = name.split( '.' ),
|
253
|
+
type = null;
|
254
|
+
|
255
|
+
_.find( this._modelScopes, function( scope ) {
|
256
|
+
type = _.reduce( parts, function( memo, val ) {
|
257
|
+
return memo[ val ];
|
258
|
+
}, scope );
|
259
|
+
|
260
|
+
if ( type && type !== scope ) {
|
261
|
+
return true;
|
262
|
+
}
|
263
|
+
}, this );
|
264
|
+
|
265
|
+
return type;
|
192
266
|
},
|
193
267
|
|
194
268
|
_createCollection: function( type ) {
|
195
269
|
var coll;
|
196
270
|
|
197
|
-
// If 'type' is an instance, take
|
271
|
+
// If 'type' is an instance, take its constructor
|
198
272
|
if ( type instanceof Backbone.RelationalModel ) {
|
199
273
|
type = type.constructor;
|
200
274
|
}
|
201
275
|
|
202
276
|
// Type should inherit from Backbone.RelationalModel.
|
203
|
-
if ( type.prototype instanceof Backbone.RelationalModel
|
277
|
+
if ( type.prototype instanceof Backbone.RelationalModel ) {
|
204
278
|
coll = new Backbone.Collection();
|
205
279
|
coll.model = type;
|
206
280
|
|
@@ -209,10 +283,53 @@
|
|
209
283
|
|
210
284
|
return coll;
|
211
285
|
},
|
212
|
-
|
213
|
-
|
286
|
+
|
287
|
+
/**
|
288
|
+
* Find the attribute that is to be used as the `id` on a given object
|
289
|
+
* @param type
|
290
|
+
* @param {String|Number|Object|Backbone.RelationalModel} item
|
291
|
+
* @return {String|Number}
|
292
|
+
*/
|
293
|
+
resolveIdForItem: function( type, item ) {
|
294
|
+
var id = _.isString( item ) || _.isNumber( item ) ? item : null;
|
295
|
+
|
296
|
+
if ( id === null ) {
|
297
|
+
if ( item instanceof Backbone.RelationalModel ) {
|
298
|
+
id = item.id;
|
299
|
+
}
|
300
|
+
else if ( _.isObject( item ) ) {
|
301
|
+
id = item[ type.prototype.idAttribute ];
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
// Make all falsy values `null` (except for 0, which could be an id.. see '/issues/179')
|
306
|
+
if ( !id && id !== 0 ) {
|
307
|
+
id = null;
|
308
|
+
}
|
309
|
+
|
310
|
+
return id;
|
311
|
+
},
|
312
|
+
|
313
|
+
/**
|
314
|
+
*
|
315
|
+
* @param type
|
316
|
+
* @param {String|Number|Object|Backbone.RelationalModel} item
|
317
|
+
*/
|
318
|
+
find: function( type, item ) {
|
319
|
+
var id = this.resolveIdForItem( type, item );
|
214
320
|
var coll = this.getCollection( type );
|
215
|
-
|
321
|
+
|
322
|
+
// Because the found object could be of any of the type's superModel
|
323
|
+
// types, only return it if it's actually of the type asked for.
|
324
|
+
if ( coll ) {
|
325
|
+
var obj = coll.get( id );
|
326
|
+
|
327
|
+
if ( obj instanceof type ) {
|
328
|
+
return obj;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
return null;
|
216
333
|
},
|
217
334
|
|
218
335
|
/**
|
@@ -253,19 +370,19 @@
|
|
253
370
|
* are used to regulate addition and removal of models from relations.
|
254
371
|
*
|
255
372
|
* @param {Backbone.RelationalModel} instance
|
256
|
-
* @param {
|
373
|
+
* @param {Object} options
|
257
374
|
* @param {string} options.key
|
258
375
|
* @param {Backbone.RelationalModel.constructor} options.relatedModel
|
259
|
-
* @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON,
|
376
|
+
* @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids.
|
260
377
|
* @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store.
|
261
|
-
* @param {
|
378
|
+
* @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
|
262
379
|
* the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
|
263
380
|
* {Backbone.Relation|String} type ('HasOne' or 'HasMany').
|
264
381
|
*/
|
265
382
|
Backbone.Relation = function( instance, options ) {
|
266
383
|
this.instance = instance;
|
267
384
|
// Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
|
268
|
-
options = (
|
385
|
+
options = _.isObject( options ) ? options : {};
|
269
386
|
this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
|
270
387
|
this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
|
271
388
|
Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
|
@@ -273,6 +390,8 @@
|
|
273
390
|
this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
|
274
391
|
|
275
392
|
this.key = this.options.key;
|
393
|
+
this.keySource = this.options.keySource || this.key;
|
394
|
+
this.keyDestination = this.options.keyDestination || this.keySource || this.key;
|
276
395
|
|
277
396
|
// 'exports' should be the global object where 'relatedModel' can be found on if given as a string.
|
278
397
|
this.relatedModel = this.options.relatedModel;
|
@@ -284,8 +403,14 @@
|
|
284
403
|
return false;
|
285
404
|
}
|
286
405
|
|
287
|
-
if(instance) {
|
288
|
-
this.keyContents = this.instance.get( this.
|
406
|
+
if ( instance ) {
|
407
|
+
this.keyContents = this.instance.get( this.keySource );
|
408
|
+
|
409
|
+
// Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
|
410
|
+
if ( this.key !== this.keySource ) {
|
411
|
+
this.instance.unset( this.keySource, { silent: true } );
|
412
|
+
}
|
413
|
+
|
289
414
|
// Add this Relation to instance._relations
|
290
415
|
this.instance._relations.push( this );
|
291
416
|
}
|
@@ -304,7 +429,7 @@
|
|
304
429
|
|
305
430
|
_.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' );
|
306
431
|
|
307
|
-
if( instance ) {
|
432
|
+
if ( instance ) {
|
308
433
|
this.initialize();
|
309
434
|
|
310
435
|
// When a model in the store is destroyed, check if it is 'this.instance'.
|
@@ -358,64 +483,68 @@
|
|
358
483
|
* @return {Boolean} True if pre-conditions are satisfied, false if they're not.
|
359
484
|
*/
|
360
485
|
checkPreconditions: function() {
|
361
|
-
var i = this.instance,
|
486
|
+
var i = this.instance,
|
487
|
+
k = this.key,
|
488
|
+
m = this.model,
|
489
|
+
rm = this.relatedModel,
|
490
|
+
warn = Backbone.Relational.showWarnings && typeof console !== 'undefined';
|
491
|
+
|
362
492
|
if ( !m || !k || !rm ) {
|
363
|
-
warn &&
|
493
|
+
warn && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, m, k, rm );
|
364
494
|
return false;
|
365
495
|
}
|
366
|
-
// Check if the type in '
|
367
|
-
if ( !( m.prototype instanceof Backbone.RelationalModel
|
368
|
-
warn &&
|
496
|
+
// Check if the type in 'model' inherits from Backbone.RelationalModel
|
497
|
+
if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
|
498
|
+
warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i );
|
369
499
|
return false;
|
370
500
|
}
|
371
501
|
// Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
|
372
|
-
if ( !( rm.prototype instanceof Backbone.RelationalModel
|
373
|
-
warn &&
|
502
|
+
if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
|
503
|
+
warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
|
374
504
|
return false;
|
375
505
|
}
|
376
506
|
// Check if this is not a HasMany, and the reverse relation is HasMany as well
|
377
|
-
if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany
|
378
|
-
warn &&
|
507
|
+
if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
|
508
|
+
warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
|
379
509
|
return false;
|
380
510
|
}
|
381
511
|
|
382
|
-
if
|
383
|
-
|
384
|
-
|
385
|
-
var exists = _.any( i._relations, function( rel ) {
|
512
|
+
// Check if we're not attempting to create a duplicate relationship
|
513
|
+
if ( i && i._relations.length ) {
|
514
|
+
var exists = _.any( i._relations, function( rel ) {
|
386
515
|
var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key;
|
387
516
|
return rel.relatedModel === rm && rel.key === k &&
|
388
517
|
( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key );
|
389
518
|
}, this );
|
390
519
|
|
391
|
-
|
392
|
-
|
520
|
+
if ( exists ) {
|
521
|
+
warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
|
393
522
|
this, i, k, rm, this.reverseRelation.key );
|
394
|
-
|
395
|
-
}
|
523
|
+
return false;
|
396
524
|
}
|
397
525
|
}
|
526
|
+
|
398
527
|
return true;
|
399
528
|
},
|
400
|
-
|
529
|
+
|
530
|
+
/**
|
531
|
+
* Set the related model(s) for this relation
|
532
|
+
* @param {Backbone.Mode|Backbone.Collection} related
|
533
|
+
* @param {Object} [options]
|
534
|
+
*/
|
401
535
|
setRelated: function( related, options ) {
|
402
536
|
this.related = related;
|
403
|
-
|
404
|
-
value[ this.key ] = related;
|
537
|
+
|
405
538
|
this.instance.acquire();
|
406
|
-
this.instance.set(
|
539
|
+
this.instance.set( this.key, related, _.defaults( options || {}, { silent: true } ) );
|
407
540
|
this.instance.release();
|
408
541
|
},
|
409
542
|
|
410
|
-
createModel: function( item ) {
|
411
|
-
if ( this.options.createModels && typeof( item ) === 'object' ) {
|
412
|
-
return new this.relatedModel( item );
|
413
|
-
}
|
414
|
-
},
|
415
|
-
|
416
543
|
/**
|
417
544
|
* Determine if a relation (on a different RelationalModel) is the reverse
|
418
545
|
* relation of the current one.
|
546
|
+
* @param {Backbone.Relation} relation
|
547
|
+
* @return {Boolean}
|
419
548
|
*/
|
420
549
|
_isReverseRelation: function( relation ) {
|
421
550
|
if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
|
@@ -429,34 +558,52 @@
|
|
429
558
|
* Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
|
430
559
|
* @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
|
431
560
|
* If not specified, 'this.related' is used.
|
561
|
+
* @return {Backbone.Relation[]}
|
432
562
|
*/
|
433
563
|
getReverseRelations: function( model ) {
|
434
564
|
var reverseRelations = [];
|
435
565
|
// Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
|
436
566
|
var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
|
437
567
|
_.each( models , function( related ) {
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
568
|
+
_.each( related.getRelations(), function( relation ) {
|
569
|
+
if ( this._isReverseRelation( relation ) ) {
|
570
|
+
reverseRelations.push( relation );
|
571
|
+
}
|
572
|
+
}, this );
|
442
573
|
}, this );
|
443
|
-
}, this );
|
444
574
|
|
445
575
|
return reverseRelations;
|
446
576
|
},
|
447
577
|
|
448
578
|
/**
|
449
|
-
* Rename options.silent, so
|
579
|
+
* Rename options.silent to options.silentChange, so events propagate properly.
|
450
580
|
* (for example in HasMany, from 'addRelated'->'handleAddition')
|
581
|
+
* @param {Object} [options]
|
582
|
+
* @return {Object}
|
451
583
|
*/
|
452
584
|
sanitizeOptions: function( options ) {
|
453
|
-
options
|
585
|
+
options = options ? _.clone( options ) : {};
|
454
586
|
if ( options.silent ) {
|
455
|
-
options =
|
587
|
+
options.silentChange = true;
|
456
588
|
delete options.silent;
|
457
589
|
}
|
458
590
|
return options;
|
459
591
|
},
|
592
|
+
|
593
|
+
/**
|
594
|
+
* Rename options.silentChange to options.silent, so events are silenced as intended in Backbone's
|
595
|
+
* original functions.
|
596
|
+
* @param {Object} [options]
|
597
|
+
* @return {Object}
|
598
|
+
*/
|
599
|
+
unsanitizeOptions: function( options ) {
|
600
|
+
options = options ? _.clone( options ) : {};
|
601
|
+
if ( options.silentChange ) {
|
602
|
+
options.silent = true;
|
603
|
+
delete options.silentChange;
|
604
|
+
}
|
605
|
+
return options;
|
606
|
+
},
|
460
607
|
|
461
608
|
// Cleanup. Get reverse relation, call removeRelated on each.
|
462
609
|
destroy: function() {
|
@@ -487,10 +634,9 @@
|
|
487
634
|
this.setRelated( model );
|
488
635
|
|
489
636
|
// Notify new 'related' object of the new relation.
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
} );
|
637
|
+
_.each( this.getReverseRelations(), function( relation ) {
|
638
|
+
relation.addRelated( this.instance );
|
639
|
+
}, this );
|
494
640
|
},
|
495
641
|
|
496
642
|
findRelated: function( options ) {
|
@@ -500,16 +646,8 @@
|
|
500
646
|
if ( item instanceof this.relatedModel ) {
|
501
647
|
model = item;
|
502
648
|
}
|
503
|
-
else if ( item
|
504
|
-
|
505
|
-
var id = _.isString( item ) || _.isNumber( item ) ? item : item[ this.relatedModel.prototype.idAttribute ];
|
506
|
-
model = Backbone.Relational.store.find( this.relatedModel, id );
|
507
|
-
if ( model && _.isObject( item ) ) {
|
508
|
-
model.set( item, options );
|
509
|
-
}
|
510
|
-
else if ( !model ) {
|
511
|
-
model = this.createModel( item );
|
512
|
-
}
|
649
|
+
else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
|
650
|
+
model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
|
513
651
|
}
|
514
652
|
|
515
653
|
return model;
|
@@ -519,7 +657,7 @@
|
|
519
657
|
* If the key is changed, notify old & new reverse relations and initialize the new relation
|
520
658
|
*/
|
521
659
|
onChange: function( model, attr, options ) {
|
522
|
-
// Don't accept recursive calls to onChange (like onChange->findRelated->
|
660
|
+
// Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
|
523
661
|
if ( this.isLocked() ) {
|
524
662
|
return;
|
525
663
|
}
|
@@ -582,9 +720,9 @@
|
|
582
720
|
options = this.sanitizeOptions( options );
|
583
721
|
|
584
722
|
var item = this.keyContents;
|
585
|
-
if ( item
|
586
|
-
var id =
|
587
|
-
if ( model.id === id ) {
|
723
|
+
if ( item || item === 0 ) { // since 0 can be a valid `id` as well
|
724
|
+
var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
|
725
|
+
if ( !_.isNull( id ) && model.id === id ) {
|
588
726
|
this.addRelated( model, options );
|
589
727
|
}
|
590
728
|
}
|
@@ -617,7 +755,8 @@
|
|
617
755
|
options: {
|
618
756
|
reverseRelation: { type: 'HasOne' },
|
619
757
|
collectionType: Backbone.Collection,
|
620
|
-
collectionKey: true
|
758
|
+
collectionKey: true,
|
759
|
+
collectionOptions: {}
|
621
760
|
},
|
622
761
|
|
623
762
|
initialize: function() {
|
@@ -626,37 +765,58 @@
|
|
626
765
|
|
627
766
|
// Handle a custom 'collectionType'
|
628
767
|
this.collectionType = this.options.collectionType;
|
629
|
-
if ( _( this.collectionType )
|
768
|
+
if ( _.isString( this.collectionType ) ) {
|
630
769
|
this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
|
631
770
|
}
|
632
|
-
if ( !this.collectionType.prototype instanceof Backbone.Collection
|
771
|
+
if ( !this.collectionType.prototype instanceof Backbone.Collection ){
|
633
772
|
throw new Error( 'collectionType must inherit from Backbone.Collection' );
|
634
773
|
}
|
635
|
-
|
636
|
-
|
774
|
+
|
775
|
+
// Handle cases where a model/relation is created with a collection passed straight into 'attributes'
|
776
|
+
if ( this.keyContents instanceof Backbone.Collection ) {
|
777
|
+
this.setRelated( this._prepareCollection( this.keyContents ) );
|
778
|
+
}
|
779
|
+
else {
|
780
|
+
this.setRelated( this._prepareCollection() );
|
781
|
+
}
|
782
|
+
|
637
783
|
this.findRelated( { silent: true } );
|
638
784
|
},
|
639
785
|
|
640
|
-
|
786
|
+
_getCollectionOptions: function() {
|
787
|
+
return _.isFunction( this.options.collectionOptions ) ?
|
788
|
+
this.options.collectionOptions( this.instance ) :
|
789
|
+
this.options.collectionOptions;
|
790
|
+
},
|
791
|
+
|
792
|
+
/**
|
793
|
+
* Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany.
|
794
|
+
* If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option.
|
795
|
+
* @param {Backbone.Collection} [collection]
|
796
|
+
*/
|
797
|
+
_prepareCollection: function( collection ) {
|
641
798
|
if ( this.related ) {
|
642
799
|
this.related
|
643
800
|
.unbind( 'relational:add', this.handleAddition )
|
644
801
|
.unbind( 'relational:remove', this.handleRemoval )
|
645
802
|
.unbind( 'relational:reset', this.handleReset )
|
646
803
|
}
|
647
|
-
|
648
|
-
collection.
|
804
|
+
|
805
|
+
if ( !collection || !( collection instanceof Backbone.Collection ) ) {
|
806
|
+
collection = new this.collectionType( [], this._getCollectionOptions() );
|
807
|
+
}
|
808
|
+
|
649
809
|
collection.model = this.relatedModel;
|
650
810
|
|
651
811
|
if ( this.options.collectionKey ) {
|
652
812
|
var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey;
|
653
813
|
|
654
|
-
if (collection[ key ] && collection[ key ] !== this.instance ) {
|
814
|
+
if ( collection[ key ] && collection[ key ] !== this.instance ) {
|
655
815
|
if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
|
656
816
|
console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey );
|
657
817
|
}
|
658
818
|
}
|
659
|
-
else {
|
819
|
+
else if ( key ) {
|
660
820
|
collection[ key ] = this.instance;
|
661
821
|
}
|
662
822
|
}
|
@@ -671,25 +831,36 @@
|
|
671
831
|
|
672
832
|
findRelated: function( options ) {
|
673
833
|
if ( this.keyContents ) {
|
674
|
-
|
675
|
-
this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ];
|
834
|
+
var models = [];
|
676
835
|
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
836
|
+
if ( this.keyContents instanceof Backbone.Collection ) {
|
837
|
+
models = this.keyContents.models;
|
838
|
+
}
|
839
|
+
else {
|
840
|
+
// Handle cases the an API/user supplies just an Object/id instead of an Array
|
841
|
+
this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ];
|
842
|
+
|
843
|
+
// Try to find instances of the appropriate 'relatedModel' in the store
|
844
|
+
_.each( this.keyContents, function( item ) {
|
845
|
+
var model = null;
|
846
|
+
if ( item instanceof this.relatedModel ) {
|
847
|
+
model = item;
|
848
|
+
}
|
849
|
+
else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
|
850
|
+
model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
|
851
|
+
}
|
852
|
+
|
853
|
+
if ( model && !this.related.getByCid( model ) && !this.related.get( model ) ) {
|
854
|
+
models.push( model );
|
855
|
+
}
|
856
|
+
}, this );
|
857
|
+
}
|
858
|
+
|
859
|
+
// Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.)
|
860
|
+
if ( models.length ) {
|
861
|
+
options = this.unsanitizeOptions( options );
|
862
|
+
this.related.add( models, options );
|
863
|
+
}
|
693
864
|
}
|
694
865
|
},
|
695
866
|
|
@@ -707,14 +878,24 @@
|
|
707
878
|
|
708
879
|
// Replace 'this.related' by 'attr' if it is a Backbone.Collection
|
709
880
|
if ( attr instanceof Backbone.Collection ) {
|
710
|
-
this.
|
881
|
+
this._prepareCollection( attr );
|
711
882
|
this.related = attr;
|
712
883
|
}
|
713
884
|
// Otherwise, 'attr' should be an array of related object ids.
|
714
|
-
// Re-use the current 'this.related' if it is a Backbone.Collection.
|
885
|
+
// Re-use the current 'this.related' if it is a Backbone.Collection, and remove any current entries.
|
886
|
+
// Otherwise, create a new collection.
|
715
887
|
else {
|
716
|
-
var coll
|
717
|
-
|
888
|
+
var coll;
|
889
|
+
|
890
|
+
if ( this.related instanceof Backbone.Collection ) {
|
891
|
+
coll = this.related;
|
892
|
+
coll.remove( coll.models );
|
893
|
+
}
|
894
|
+
else {
|
895
|
+
coll = this._prepareCollection();
|
896
|
+
}
|
897
|
+
|
898
|
+
this.setRelated( coll );
|
718
899
|
this.findRelated( options );
|
719
900
|
}
|
720
901
|
|
@@ -734,9 +915,9 @@
|
|
734
915
|
if ( !this.related.getByCid( model ) && !this.related.get( model ) ) {
|
735
916
|
// Check if this new model was specified in 'this.keyContents'
|
736
917
|
var item = _.any( this.keyContents, function( item ) {
|
737
|
-
|
738
|
-
|
739
|
-
|
918
|
+
var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
|
919
|
+
return !_.isNull( id ) && id === model.id;
|
920
|
+
}, this );
|
740
921
|
|
741
922
|
if ( item ) {
|
742
923
|
this.related.add( model, options );
|
@@ -752,7 +933,7 @@
|
|
752
933
|
//console.debug('handleAddition called; args=%o', arguments);
|
753
934
|
// Make sure the model is in fact a valid model before continuing.
|
754
935
|
// (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel)
|
755
|
-
if( !( model instanceof Backbone.Model ) ) {
|
936
|
+
if ( !( model instanceof Backbone.Model ) ) {
|
756
937
|
return;
|
757
938
|
}
|
758
939
|
|
@@ -775,7 +956,7 @@
|
|
775
956
|
*/
|
776
957
|
handleRemoval: function( model, coll, options ) {
|
777
958
|
//console.debug('handleRemoval called; args=%o', arguments);
|
778
|
-
if( !( model instanceof Backbone.Model ) ) {
|
959
|
+
if ( !( model instanceof Backbone.Model ) ) {
|
779
960
|
return;
|
780
961
|
}
|
781
962
|
|
@@ -802,6 +983,7 @@
|
|
802
983
|
|
803
984
|
addRelated: function( model, options ) {
|
804
985
|
var dit = this;
|
986
|
+
options = this.unsanitizeOptions( options );
|
805
987
|
model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice
|
806
988
|
if ( dit.related && !dit.related.getByCid( model ) && !dit.related.get( model ) ) {
|
807
989
|
dit.related.add( model, options );
|
@@ -810,6 +992,7 @@
|
|
810
992
|
},
|
811
993
|
|
812
994
|
removeRelated: function( model, options ) {
|
995
|
+
options = this.unsanitizeOptions( options );
|
813
996
|
if ( this.related.getByCid( model ) || this.related.get( model ) ) {
|
814
997
|
this.related.remove( model, options );
|
815
998
|
}
|
@@ -830,6 +1013,9 @@
|
|
830
1013
|
_deferProcessing: false,
|
831
1014
|
_queue: null,
|
832
1015
|
|
1016
|
+
subModelTypeAttribute: 'type',
|
1017
|
+
subModelTypes: null,
|
1018
|
+
|
833
1019
|
constructor: function( attributes, options ) {
|
834
1020
|
// Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
|
835
1021
|
// Defer 'processQueue', so that when 'Relation.createModels' is used we:
|
@@ -861,7 +1047,7 @@
|
|
861
1047
|
this._queue.block();
|
862
1048
|
Backbone.Relational.eventQueue.block();
|
863
1049
|
|
864
|
-
Backbone.Model.
|
1050
|
+
Backbone.Model.apply( this, arguments );
|
865
1051
|
|
866
1052
|
// Try to run the global queue holding external events
|
867
1053
|
Backbone.Relational.eventQueue.unblock();
|
@@ -894,7 +1080,7 @@
|
|
894
1080
|
|
895
1081
|
_.each( this.relations, function( rel ) {
|
896
1082
|
var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
|
897
|
-
if ( type && type.prototype instanceof Backbone.Relation
|
1083
|
+
if ( type && type.prototype instanceof Backbone.Relation ) {
|
898
1084
|
new type( this, rel ); // Also pushes the new Relation into _relations
|
899
1085
|
}
|
900
1086
|
else {
|
@@ -906,17 +1092,18 @@
|
|
906
1092
|
this.release();
|
907
1093
|
this.processQueue();
|
908
1094
|
},
|
909
|
-
|
1095
|
+
|
910
1096
|
/**
|
911
1097
|
* When new values are set, notify this model's relations (also if options.silent is set).
|
912
1098
|
* (Relation.setRelated locks this model before calling 'set' on it to prevent loops)
|
913
1099
|
*/
|
914
1100
|
updateRelations: function( options ) {
|
915
|
-
if( this._isInitialized && !this.isLocked() ) {
|
1101
|
+
if ( this._isInitialized && !this.isLocked() ) {
|
916
1102
|
_.each( this._relations, function( rel ) {
|
917
|
-
|
1103
|
+
// Update from data in `rel.keySource` if set, or `rel.key` otherwise
|
1104
|
+
var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ];
|
918
1105
|
if ( rel.related !== val ) {
|
919
|
-
this.trigger('relational:change:' + rel.key, this, val, options || {} );
|
1106
|
+
this.trigger( 'relational:change:' + rel.key, this, val, options || {} );
|
920
1107
|
}
|
921
1108
|
}, this );
|
922
1109
|
}
|
@@ -962,18 +1149,19 @@
|
|
962
1149
|
/**
|
963
1150
|
* Retrieve related objects.
|
964
1151
|
* @param key {string} The relation key to fetch models for.
|
965
|
-
* @param options {
|
1152
|
+
* @param options {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
|
1153
|
+
* @param update {boolean} Whether to force a fetch from the server (updating existing models).
|
966
1154
|
* @return {jQuery.when[]} An array of request objects
|
967
1155
|
*/
|
968
|
-
fetchRelated: function( key, options ) {
|
1156
|
+
fetchRelated: function( key, options, update ) {
|
969
1157
|
options || ( options = {} );
|
970
1158
|
var setUrl,
|
971
1159
|
requests = [],
|
972
1160
|
rel = this.getRelation( key ),
|
973
1161
|
keyContents = rel && rel.keyContents,
|
974
1162
|
toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) {
|
975
|
-
var id =
|
976
|
-
return id && !Backbone.Relational.store.find( rel.relatedModel, id );
|
1163
|
+
var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item );
|
1164
|
+
return !_.isNull( id ) && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) );
|
977
1165
|
}, this );
|
978
1166
|
|
979
1167
|
if ( toFetch && toFetch.length ) {
|
@@ -981,13 +1169,13 @@
|
|
981
1169
|
var models = _.map( toFetch, function( item ) {
|
982
1170
|
var model;
|
983
1171
|
|
984
|
-
if (
|
985
|
-
model =
|
1172
|
+
if ( _.isObject( item ) ) {
|
1173
|
+
model = rel.relatedModel.build( item );
|
986
1174
|
}
|
987
1175
|
else {
|
988
1176
|
var attrs = {};
|
989
1177
|
attrs[ rel.relatedModel.prototype.idAttribute ] = item;
|
990
|
-
model =
|
1178
|
+
model = rel.relatedModel.build( attrs );
|
991
1179
|
}
|
992
1180
|
|
993
1181
|
return model;
|
@@ -1007,9 +1195,9 @@
|
|
1007
1195
|
error: function() {
|
1008
1196
|
var args = arguments;
|
1009
1197
|
_.each( models, function( model ) {
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1198
|
+
model.trigger( 'destroy', model, model.collection, options );
|
1199
|
+
options.error && options.error.apply( model, args );
|
1200
|
+
});
|
1013
1201
|
},
|
1014
1202
|
url: setUrl
|
1015
1203
|
},
|
@@ -1043,7 +1231,7 @@
|
|
1043
1231
|
|
1044
1232
|
// Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
|
1045
1233
|
var attributes;
|
1046
|
-
if (_.isObject( key ) || key == null) {
|
1234
|
+
if ( _.isObject( key ) || key == null ) {
|
1047
1235
|
attributes = key;
|
1048
1236
|
options = value;
|
1049
1237
|
}
|
@@ -1054,10 +1242,12 @@
|
|
1054
1242
|
|
1055
1243
|
var result = Backbone.Model.prototype.set.apply( this, arguments );
|
1056
1244
|
|
1057
|
-
// 'set' is called quite late in 'Backbone.Model.prototype.constructor', but before 'initialize'.
|
1058
1245
|
// Ideal place to set up relations :)
|
1059
1246
|
if ( !this._isInitialized && !this.isLocked() ) {
|
1247
|
+
this.constructor.initializeModelHierarchy();
|
1248
|
+
|
1060
1249
|
Backbone.Relational.store.register( this );
|
1250
|
+
|
1061
1251
|
this.initializeRelations();
|
1062
1252
|
}
|
1063
1253
|
// Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
|
@@ -1104,9 +1294,9 @@
|
|
1104
1294
|
* and 'previousAttributes' will be available when the event is fired.
|
1105
1295
|
*/
|
1106
1296
|
change: function( options ) {
|
1107
|
-
var dit = this;
|
1297
|
+
var dit = this, args = arguments;
|
1108
1298
|
Backbone.Relational.eventQueue.add( function() {
|
1109
|
-
Backbone.Model.prototype.change.apply( dit,
|
1299
|
+
Backbone.Model.prototype.change.apply( dit, args );
|
1110
1300
|
});
|
1111
1301
|
},
|
1112
1302
|
|
@@ -1117,8 +1307,8 @@
|
|
1117
1307
|
}
|
1118
1308
|
|
1119
1309
|
_.each( this.getRelations(), function( rel ) {
|
1120
|
-
|
1121
|
-
|
1310
|
+
delete attributes[ rel.key ];
|
1311
|
+
});
|
1122
1312
|
|
1123
1313
|
return new this.constructor( attributes );
|
1124
1314
|
},
|
@@ -1135,61 +1325,275 @@
|
|
1135
1325
|
this.acquire();
|
1136
1326
|
var json = Backbone.Model.prototype.toJSON.call( this );
|
1137
1327
|
|
1328
|
+
if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) {
|
1329
|
+
json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue;
|
1330
|
+
}
|
1331
|
+
|
1138
1332
|
_.each( this._relations, function( rel ) {
|
1139
1333
|
var value = json[ rel.key ];
|
1140
|
-
|
1141
|
-
if ( rel.options.includeInJSON === true
|
1142
|
-
|
1334
|
+
|
1335
|
+
if ( rel.options.includeInJSON === true) {
|
1336
|
+
if ( value && _.isFunction( value.toJSON ) ) {
|
1337
|
+
json[ rel.keyDestination ] = value.toJSON();
|
1338
|
+
}
|
1339
|
+
else {
|
1340
|
+
json[ rel.keyDestination ] = null;
|
1341
|
+
}
|
1143
1342
|
}
|
1144
1343
|
else if ( _.isString( rel.options.includeInJSON ) ) {
|
1145
1344
|
if ( value instanceof Backbone.Collection ) {
|
1146
|
-
json[ rel.
|
1345
|
+
json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON );
|
1147
1346
|
}
|
1148
1347
|
else if ( value instanceof Backbone.Model ) {
|
1149
|
-
json[ rel.
|
1348
|
+
json[ rel.keyDestination ] = value.get( rel.options.includeInJSON );
|
1349
|
+
}
|
1350
|
+
else {
|
1351
|
+
json[ rel.keyDestination ] = null;
|
1352
|
+
}
|
1353
|
+
}
|
1354
|
+
else if ( _.isArray( rel.options.includeInJSON ) ) {
|
1355
|
+
if ( value instanceof Backbone.Collection ) {
|
1356
|
+
var valueSub = [];
|
1357
|
+
value.each( function( model ) {
|
1358
|
+
var curJson = {};
|
1359
|
+
_.each( rel.options.includeInJSON, function( key ) {
|
1360
|
+
curJson[ key ] = model.get( key );
|
1361
|
+
});
|
1362
|
+
valueSub.push( curJson );
|
1363
|
+
});
|
1364
|
+
json[ rel.keyDestination ] = valueSub;
|
1365
|
+
}
|
1366
|
+
else if ( value instanceof Backbone.Model ) {
|
1367
|
+
var valueSub = {};
|
1368
|
+
_.each( rel.options.includeInJSON, function( key ) {
|
1369
|
+
valueSub[ key ] = value.get( key );
|
1370
|
+
});
|
1371
|
+
json[ rel.keyDestination ] = valueSub;
|
1372
|
+
}
|
1373
|
+
else {
|
1374
|
+
json[ rel.keyDestination ] = null;
|
1150
1375
|
}
|
1151
1376
|
}
|
1152
1377
|
else {
|
1153
1378
|
delete json[ rel.key ];
|
1154
1379
|
}
|
1155
|
-
|
1380
|
+
|
1381
|
+
if ( rel.keyDestination !== rel.key ) {
|
1382
|
+
delete json[ rel.key ];
|
1383
|
+
}
|
1384
|
+
});
|
1156
1385
|
|
1157
1386
|
this.release();
|
1158
1387
|
return json;
|
1159
1388
|
}
|
1389
|
+
},
|
1390
|
+
{
|
1391
|
+
setup: function( superModel ) {
|
1392
|
+
// We don't want to share a relations array with a parent, as this will cause problems with
|
1393
|
+
// reverse relations.
|
1394
|
+
this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 );
|
1395
|
+
|
1396
|
+
this._subModels = {};
|
1397
|
+
this._superModel = null;
|
1398
|
+
|
1399
|
+
// If this model has 'subModelTypes' itself, remember them in the store
|
1400
|
+
if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) {
|
1401
|
+
Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this );
|
1402
|
+
}
|
1403
|
+
// The 'subModelTypes' property should not be inherited, so reset it.
|
1404
|
+
else {
|
1405
|
+
this.prototype.subModelTypes = null;
|
1406
|
+
}
|
1407
|
+
|
1408
|
+
// Initialize all reverseRelations that belong to this new model.
|
1409
|
+
_.each( this.prototype.relations, function( rel ) {
|
1410
|
+
if ( !rel.model ) {
|
1411
|
+
rel.model = this;
|
1412
|
+
}
|
1413
|
+
|
1414
|
+
if ( rel.reverseRelation && rel.model === this ) {
|
1415
|
+
var preInitialize = true;
|
1416
|
+
if ( _.isString( rel.relatedModel ) ) {
|
1417
|
+
/**
|
1418
|
+
* The related model might not be defined for two reasons
|
1419
|
+
* 1. it never gets defined, e.g. a typo
|
1420
|
+
* 2. it is related to itself
|
1421
|
+
* In neither of these cases do we need to pre-initialize reverse relations.
|
1422
|
+
*/
|
1423
|
+
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
|
1424
|
+
preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
|
1425
|
+
}
|
1426
|
+
|
1427
|
+
var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
|
1428
|
+
if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) {
|
1429
|
+
new type( null, rel );
|
1430
|
+
}
|
1431
|
+
}
|
1432
|
+
}, this );
|
1433
|
+
|
1434
|
+
return this;
|
1435
|
+
},
|
1436
|
+
|
1437
|
+
/**
|
1438
|
+
* Create a 'Backbone.Model' instance based on 'attributes'.
|
1439
|
+
* @param {Object} attributes
|
1440
|
+
* @param {Object} [options]
|
1441
|
+
* @return {Backbone.Model}
|
1442
|
+
*/
|
1443
|
+
build: function( attributes, options ) {
|
1444
|
+
var model = this;
|
1445
|
+
|
1446
|
+
// 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
|
1447
|
+
this.initializeModelHierarchy();
|
1448
|
+
|
1449
|
+
// Determine what type of (sub)model should be built if applicable.
|
1450
|
+
// Lookup the proper subModelType in 'this._subModels'.
|
1451
|
+
if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) {
|
1452
|
+
var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ];
|
1453
|
+
var subModelType = this._subModels[ subModelTypeAttribute ];
|
1454
|
+
if ( subModelType ) {
|
1455
|
+
model = subModelType;
|
1456
|
+
}
|
1457
|
+
}
|
1458
|
+
|
1459
|
+
return new model( attributes, options );
|
1460
|
+
},
|
1461
|
+
|
1462
|
+
initializeModelHierarchy: function() {
|
1463
|
+
// If we're here for the first time, try to determine if this modelType has a 'superModel'.
|
1464
|
+
if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) {
|
1465
|
+
Backbone.Relational.store.setupSuperModel( this );
|
1466
|
+
|
1467
|
+
// If a superModel has been found, copy relations from the _superModel if they haven't been
|
1468
|
+
// inherited automatically (due to a redefinition of 'relations').
|
1469
|
+
// Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail
|
1470
|
+
// the isUndefined/isNull check next time.
|
1471
|
+
if ( this._superModel ) {
|
1472
|
+
//
|
1473
|
+
if ( this._superModel.prototype.relations ) {
|
1474
|
+
var supermodelRelationsExist = _.any( this.prototype.relations, function( rel ) {
|
1475
|
+
return rel.model && rel.model !== this;
|
1476
|
+
}, this );
|
1477
|
+
|
1478
|
+
if ( !supermodelRelationsExist ) {
|
1479
|
+
this.prototype.relations = this._superModel.prototype.relations.concat( this.prototype.relations );
|
1480
|
+
}
|
1481
|
+
}
|
1482
|
+
}
|
1483
|
+
else {
|
1484
|
+
this._superModel = false;
|
1485
|
+
}
|
1486
|
+
}
|
1487
|
+
|
1488
|
+
// 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.
|
1489
|
+
if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) {
|
1490
|
+
_.each( this.prototype.subModelTypes, function( subModelTypeName ) {
|
1491
|
+
var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
|
1492
|
+
subModelType && subModelType.initializeModelHierarchy();
|
1493
|
+
});
|
1494
|
+
}
|
1495
|
+
},
|
1496
|
+
|
1497
|
+
/**
|
1498
|
+
* Find an instance of `this` type in 'Backbone.Relational.store'.
|
1499
|
+
* - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
|
1500
|
+
* - If `attributes` is an object, the model will be updated with `attributes` if found.
|
1501
|
+
* Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).
|
1502
|
+
* @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
|
1503
|
+
* @param {Object} [options]
|
1504
|
+
* @param {Boolean} [options.create=true]
|
1505
|
+
* @return {Backbone.RelationalModel}
|
1506
|
+
*/
|
1507
|
+
findOrCreate: function( attributes, options ) {
|
1508
|
+
// Try to find an instance of 'this' model type in the store
|
1509
|
+
var model = Backbone.Relational.store.find( this, attributes );
|
1510
|
+
|
1511
|
+
// If we found an instance, update it with the data in 'item'; if not, create an instance
|
1512
|
+
// (unless 'options.create' is false).
|
1513
|
+
if ( _.isObject( attributes ) ) {
|
1514
|
+
if ( model ) {
|
1515
|
+
model.set( attributes, options );
|
1516
|
+
}
|
1517
|
+
else if ( !options || ( options && options.create !== false ) ) {
|
1518
|
+
model = this.build( attributes, options );
|
1519
|
+
}
|
1520
|
+
}
|
1521
|
+
|
1522
|
+
return model;
|
1523
|
+
}
|
1160
1524
|
});
|
1161
1525
|
_.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
|
1162
1526
|
|
1527
|
+
/**
|
1528
|
+
* Override Backbone.Collection._prepareModel, so objects will be built using the correct type
|
1529
|
+
* if the collection.model has subModels.
|
1530
|
+
*/
|
1531
|
+
Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;
|
1532
|
+
Backbone.Collection.prototype._prepareModel = function ( model, options ) {
|
1533
|
+
options || (options = {});
|
1534
|
+
if ( !( model instanceof Backbone.Model ) ) {
|
1535
|
+
var attrs = model;
|
1536
|
+
options.collection = this;
|
1537
|
+
|
1538
|
+
if ( typeof this.model.build !== 'undefined' ) {
|
1539
|
+
model = this.model.build( attrs, options );
|
1540
|
+
}
|
1541
|
+
else {
|
1542
|
+
model = new this.model( attrs, options );
|
1543
|
+
}
|
1544
|
+
|
1545
|
+
if ( !model._validate( model.attributes, options ) ) {
|
1546
|
+
model = false;
|
1547
|
+
}
|
1548
|
+
}
|
1549
|
+
else if ( !model.collection ) {
|
1550
|
+
model.collection = this;
|
1551
|
+
}
|
1552
|
+
|
1553
|
+
return model;
|
1554
|
+
}
|
1555
|
+
|
1163
1556
|
/**
|
1164
1557
|
* Override Backbone.Collection.add, so objects fetched from the server multiple times will
|
1165
1558
|
* update the existing Model. Also, trigger 'relational:add'.
|
1166
1559
|
*/
|
1167
|
-
var add = Backbone.Collection.prototype.add;
|
1560
|
+
var add = Backbone.Collection.prototype.__add = Backbone.Collection.prototype.add;
|
1168
1561
|
Backbone.Collection.prototype.add = function( models, options ) {
|
1169
1562
|
options || (options = {});
|
1170
|
-
if (!_.isArray( models ) ) {
|
1563
|
+
if ( !_.isArray( models ) ) {
|
1171
1564
|
models = [ models ];
|
1172
1565
|
}
|
1173
1566
|
|
1567
|
+
var modelsToAdd = [];
|
1568
|
+
|
1174
1569
|
//console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
|
1175
1570
|
_.each( models, function( model ) {
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1571
|
+
if ( !( model instanceof Backbone.Model ) ) {
|
1572
|
+
// Try to find 'model' in Backbone.store. If it already exists, set the new properties on it.
|
1573
|
+
var existingModel = Backbone.Relational.store.find( this.model, model[ this.model.prototype.idAttribute ] );
|
1574
|
+
if ( existingModel ) {
|
1575
|
+
existingModel.set( existingModel.parse ? existingModel.parse( model ) : model, options );
|
1576
|
+
model = existingModel;
|
1577
|
+
}
|
1578
|
+
else {
|
1579
|
+
model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
|
1580
|
+
}
|
1182
1581
|
}
|
1183
|
-
|
1184
|
-
|
1582
|
+
|
1583
|
+
if ( model instanceof Backbone.Model && !this.get( model ) && !this.getByCid( model ) ) {
|
1584
|
+
modelsToAdd.push( model );
|
1185
1585
|
}
|
1186
|
-
}
|
1586
|
+
}, this );
|
1187
1587
|
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1588
|
+
|
1589
|
+
// Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
|
1590
|
+
if ( modelsToAdd.length ) {
|
1591
|
+
add.call( this, modelsToAdd, options );
|
1592
|
+
|
1593
|
+
_.each( modelsToAdd, function( model ) {
|
1594
|
+
this.trigger( 'relational:add', model, this, options );
|
1595
|
+
}, this );
|
1596
|
+
}
|
1193
1597
|
|
1194
1598
|
return this;
|
1195
1599
|
};
|
@@ -1197,22 +1601,25 @@
|
|
1197
1601
|
/**
|
1198
1602
|
* Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
|
1199
1603
|
*/
|
1200
|
-
var remove = Backbone.Collection.prototype.remove;
|
1604
|
+
var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove;
|
1201
1605
|
Backbone.Collection.prototype.remove = function( models, options ) {
|
1202
1606
|
options || (options = {});
|
1203
|
-
if (!_.isArray( models ) ) {
|
1607
|
+
if ( !_.isArray( models ) ) {
|
1204
1608
|
models = [ models ];
|
1205
1609
|
}
|
1610
|
+
else {
|
1611
|
+
models = models.slice( 0 );
|
1612
|
+
}
|
1206
1613
|
|
1207
1614
|
//console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
|
1208
1615
|
_.each( models, function( model ) {
|
1209
|
-
|
1616
|
+
model = this.getByCid( model ) || this.get( model );
|
1210
1617
|
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1618
|
+
if ( model instanceof Backbone.Model ) {
|
1619
|
+
remove.call( this, model, options );
|
1620
|
+
this.trigger('relational:remove', model, this, options);
|
1621
|
+
}
|
1622
|
+
}, this );
|
1216
1623
|
|
1217
1624
|
return this;
|
1218
1625
|
};
|
@@ -1220,10 +1627,21 @@
|
|
1220
1627
|
/**
|
1221
1628
|
* Override 'Backbone.Collection.reset' to trigger 'relational:reset'.
|
1222
1629
|
*/
|
1223
|
-
var reset = Backbone.Collection.prototype.reset;
|
1630
|
+
var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset;
|
1224
1631
|
Backbone.Collection.prototype.reset = function( models, options ) {
|
1225
1632
|
reset.call( this, models, options );
|
1226
|
-
this.trigger( 'relational:reset',
|
1633
|
+
this.trigger( 'relational:reset', this, options );
|
1634
|
+
|
1635
|
+
return this;
|
1636
|
+
};
|
1637
|
+
|
1638
|
+
/**
|
1639
|
+
* Override 'Backbone.Collection.sort' to trigger 'relational:reset'.
|
1640
|
+
*/
|
1641
|
+
var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort;
|
1642
|
+
Backbone.Collection.prototype.sort = function( options ) {
|
1643
|
+
sort.call( this, options );
|
1644
|
+
this.trigger( 'relational:reset', this, options );
|
1227
1645
|
|
1228
1646
|
return this;
|
1229
1647
|
};
|
@@ -1232,50 +1650,37 @@
|
|
1232
1650
|
* Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
|
1233
1651
|
* are ready.
|
1234
1652
|
*/
|
1235
|
-
var
|
1653
|
+
var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger;
|
1236
1654
|
Backbone.Collection.prototype.trigger = function( eventName ) {
|
1237
1655
|
if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) {
|
1238
1656
|
var dit = this, args = arguments;
|
1657
|
+
|
1658
|
+
if (eventName === 'add') {
|
1659
|
+
args = _.toArray(args);
|
1660
|
+
// the fourth argument in case of a regular add is the option object.
|
1661
|
+
// we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
|
1662
|
+
if (_.isObject(args[3])) {
|
1663
|
+
args[3] = _.clone(args[3]);
|
1664
|
+
}
|
1665
|
+
}
|
1666
|
+
|
1239
1667
|
Backbone.Relational.eventQueue.add( function() {
|
1240
|
-
|
1668
|
+
trigger.apply( dit, args );
|
1241
1669
|
});
|
1242
1670
|
}
|
1243
1671
|
else {
|
1244
|
-
|
1672
|
+
trigger.apply( this, arguments );
|
1245
1673
|
}
|
1246
1674
|
|
1247
1675
|
return this;
|
1248
1676
|
};
|
1249
1677
|
|
1250
|
-
// Override .extend() to
|
1678
|
+
// Override .extend() to automatically call .setup()
|
1251
1679
|
Backbone.RelationalModel.extend = function( protoProps, classProps ) {
|
1252
1680
|
var child = Backbone.Model.extend.apply( this, arguments );
|
1253
|
-
|
1254
|
-
|
1255
|
-
_.each( relations, function( rel ) {
|
1256
|
-
if( rel.reverseRelation ) {
|
1257
|
-
rel.model = child;
|
1258
|
-
|
1259
|
-
var preInitialize = true;
|
1260
|
-
if ( _.isString( rel.relatedModel ) ) {
|
1261
|
-
/**
|
1262
|
-
* The related model might not be defined for two reasons
|
1263
|
-
* 1. it never gets defined, e.g. a typo
|
1264
|
-
* 2. it is related to itself
|
1265
|
-
* In neither of these cases do we need to pre-initialize reverse relations.
|
1266
|
-
*/
|
1267
|
-
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
|
1268
|
-
preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel.prototype.constructor );
|
1269
|
-
}
|
1270
|
-
|
1271
|
-
var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
|
1272
|
-
if ( preInitialize && type && type.prototype instanceof Backbone.Relation.prototype.constructor ) {
|
1273
|
-
new type( null, rel );
|
1274
|
-
}
|
1275
|
-
}
|
1276
|
-
});
|
1681
|
+
|
1682
|
+
child.setup( this );
|
1277
1683
|
|
1278
1684
|
return child;
|
1279
1685
|
};
|
1280
|
-
|
1281
1686
|
})();
|