backbone-relational-rails 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
})();
|