Backbone filtering - javascript

If I have a Backbone collection and want to create a copy of that collection with certain entries filtered out, how can I do that while keeping the copied instance as a Backbone.Collection?
Example:
​
var Module = Backbone.Model.extend();
var ModuleCollection = Backbone.Collection.​extend({
model: Module
});
​var modules = new ModuleCollection;
​modules.add({foo: 'foo'​​​​​​},{foo: 'bar'});​​​​​
console.log(modules instanceof Backbone.Collection); // true
var filtered = modules.filter(function(module) {
return module.get('foo') == 'bar';
});
console.log(filtered instanceof Backbone.Collection); // false
http://jsfiddle.net/m9eTY/
In the example above, I would like filtered to be a filtered version of modules, not just an array of models.
Essentially I would like to create a method in the collection instance that can filter out certain models and return the Backbone.Collection instance, but as soon as I start filtering the iteration methods returns an array.

You can wrap the filtered array in a temporary ModuleCollection if you want, the models filtered are the same instances of the ones in the original ModuleCollection, so if the module's attribute changes, it is still referenced by both collections.
so what I suggest you do is something like:
var filtered = new ModuleCollection(modules.filter(function (module) {
return module.get('foo') == 'bar';
}));
Since Backbone 0.9.2 there is an additional method called where that does the same:
var filtered = modules.where({foo: 'bar'});
that still returns an array though, so you will still need to wrap it as such:
var filtered = new ModuleCollection(modules.where({foo: 'bar'}));

For filtering collection using backbone
To make the filter you should have a filtered function in your collection
var MyCollection = Backbone.Collection.extend ({
filtered : function () {
I suggest to use UnderScore filter which will return true for valid and false for invalid where true is what you are looking for. use this.models to get the current collection models use model.get( '' ) to get the element you want to check for
var results = _.filter( this.models, function ( model ) {
if ( model.get('foo') == 'bar' )
return true ;
return false ;
});
Then use underscore map your results and transform it to JSON like this is probally where you are getting it wrong
results = _.map( results, function( model ) { return model.toJSON() } );
Finally returning a new backbone collection with only results this is how to make a copied collection
return new Backbone.Collection( results ) ;

Related

Separating a `Backbone` collection (events issue)

So, the problem is next: I receive large collection of prices and there are prices for specific, let say, partners. Thus collection contains some kind of groups denoted by partner_id.
I filtering this collection (using collection.filter() in initialize method), to obtain different "format" of data for subsequent views.
var BasePrices = new Collections.ProductPrices( // Creating the same collection type
this.model.get('prices').filter(function (m) { // But a bit narrowed
return ~~m.get('partner_id') === 0; // leaving prices without `partner_id`
})
);
Later I pass this newly done collection to the view that is managing the list of base prices.
The problem itself is that I'm subscribing on events of this newly done collection, but events of models that remained there after .filter() are firing their events to the old collection that lays under this.model.get('prices'), but newly added models firing their events correctly (to the BasePrices collection).
I couldn't understand why its happening so. I can presume that there is smth. related with reference of model to collection (model.collection property), but why it is not updated when I created brand new collection and how to solve the issue?
If you're creating the filtered collection only to use it in a view, it is better (and more correct) to use the original collection and let the view render only the items you want. For example (inside the view class):
render: function() {
this.model.each(function(m) {
if(~m.get('partner_id') === 0)
return;
/* render m here */
});
}
The rationale is that the view represents the original collection.
(If you need several filtered views of the same collection, you can use a single view class for all of them and pass it a filter function:
initialize: function(filter) {
this.filter = filter;
}
render: function() {
this.model.each(function(m) {
if(!this.filter(m))
return;
/* render m here */
});
}
Then create a view like this: new FilteredView(function(m) {return ~~m.get('partner_id') === 0;})
And the problem was indeed in references and cloning (not cloning, actually). The point is that we need to clone everything to the new collection. Clone ... not copy, not pass (by reference as we know) - clone.
var BasePrices = new Collections.ProductPrices() // Creating the same collection type
_(this.model.get('prices').models) // Implicitly `_.chain`ing
.filter(function (m) { return ~~m.get('partner_id') === 0; }) // leaving prices without `partner_id`
.map(function (m) { return m.toJSON(); }) // converting each model to raw object
.tap(function (a) { c.add(a); }) // adding all models at once
.value(); // evaluating chain
! More elegant ways of solving this problem are highly appreciated.
UPD: Just to keep chaining consistent here is one-liner for lodash.
var BasePrices = _(this.model.get('prices').models)
.filter(function (m) { return ~~m.get('partner_id') === 0; })
.map(function (m) { return m.toJSON(); })
// creating collection passing all models in constructor
.thru(function (a) { return new Collections.ProductPrices(a); })
.value();

How can I make Ember.js handlebars #each iterate over objects?

I'm trying to make the {{#each}} helper to iterate over an object, like in vanilla handlebars. Unfortunately if I use #each on an object, Ember.js version gives me this error:
Assertion failed: The value that #each loops over must be an Array. You passed [object Object]
I wrote this helper in attempt to remedy this:
Ember.Handlebars.helper('every', function (context, options) {
var oArray = [];
for (var k in context) {
oArray.push({
key : k,
value : context[k]
})
}
return Ember.Handlebars.helpers.each(oArray, options);
});
Now, when I attempt to use {{#every}}, I get the following error:
Assertion failed: registerBoundHelper-generated helpers do not support use with Handlebars blocks.
This seems like a basic feature, and I know I'm probably missing something obvious. Can anyone help?
Edit:
Here's a fiddle: http://jsfiddle.net/CbV8X/
Use {{each-in}} helper. You can use it like like {{each}} helper.
Example:
{{#each-in modelWhichIsObject as |key value|}}
`{{key}}`:`{{value}}`
{{/each-in}}
JS Bin demo.
After fiddling with it for a few hours, I came up with this hacky way:
Ember.Handlebars.registerHelper('every', function(context, options) {
var oArray = [], actualData = this.get(context);
for (var k in actualData) {
oArray.push({
key: k,
value: actualData[k]
})
}
this.set(context, oArray);
return Ember.Handlebars.helpers.each.apply(this,
Array.prototype.slice.call(arguments));
});
I don't know what repercussions this.set has, but this seems to work!
Here's a fiddle: http://jsfiddle.net/CbV8X/1/
I've been after similar functionality, and since we're sharing our hacky ways, here's my fiddle for the impatient: http://jsfiddle.net/L6axcob8/1/
This fiddle is based on the one provided by #lxe, with updates by #Kingpin2k, and then myself.
Ember: 1.9.1, Handlebars: 2.0.0, jQuery 2.1.3
Here we are adding a helper called every which can iterate over objects and arrays.
For example this model:
model: function() {
return {
properties: {
foo: 'bar',
zoo: 'zar'
}
};
}
can be iterated with the following handlebars template:
<ul class="properties">
{{#every p in properties}}
<li>{{p.key}} : {{p.value}}</li>
{{/every}}
</ul>
every helper works by creating an array from the objects keys, and then coordinating changes to Ember by way of an ArrayController. Yeah, hacky. This does however, let us add/remove properties to/from an object provided that object supports observation of the [] property.
In my use case I have an Ember.Object derived class which notifies [] when properties are added/removed. I'd recommend looking at Ember.Set for this functionality, although I see that Set been recently deprecated. As this is slightly out of this questions scope I'll leave it as an exercise for the reader. Here's a tip: setUnknownProperty
To be notified of property changes we wrap non-object values in what I've called a DataValueObserver which sets up (currently one way) bindings. These bindings provide a bridge between the values held by our internal ArrayController and the object we are observing.
When dealing with objects; we wrap those in ObjectProxy's so that we can introduce a 'key' member without the need to modify the object itself. Why yes, this does imply that you could use #every recursively. Another exercise for the reader ;-)
I'd recommend having your model be based around Ember.Object to be consistent with the rest of Ember, allowing you to manipulate your model via its get & set handlers. Alternatively, as demonstrated in the fiddle, you can use Em.Get/Em.set to access models, as long as you are consistent in doing so. If you touch your model directly (no get/set), then every won't be notified of your change.
Em.set(model.properties, 'foo', 'asdfsdf');
For completeness here's my every helper:
var DataValueObserver = Ember.Object.extend({
init: function() {
this._super();
// one way binding (for now)
Em.addObserver(this.parent, this.key, this, 'valueChanged');
},
value: function() {
return Em.get(this.parent, this.key);
}.property(),
valueChanged: function() {
this.notifyPropertyChange('value');
}
});
Handlebars.registerHelper("every", function() {
var args = [].slice.call(arguments);
var options = args.pop();
var context = (options.contexts && options.contexts[0]) || this;
Ember.assert("Must be in the form #every foo in bar ", 3 == args.length && args[1] === "in");
options.hash.keyword = args[0];
var property = args[2];
// if we're dealing with an array we can just forward onto the collection helper directly
var p = this.get(property);
if (Ember.Array.detect(p)) {
options.hash.dataSource = p;
return Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
}
// create an array that we will manage with content
var array = Em.ArrayController.create();
options.hash.dataSource = array;
Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
//
var update_array = function(result) {
if (!result) {
array.clear();
return;
}
// check for proxy object
var result = (result.isProxy && result.content) ? result.content : result;
var items = result;
var keys = Ember.keys(items).sort();
// iterate through sorted array, inserting & removing any mismatches
var i = 0;
for ( ; i < keys.length; ++i) {
var key = keys[i];
var value = items[key];
while (true) {
var old_obj = array.objectAt(i);
if (old_obj) {
Ember.assert("Assume that all objects in our array have a key", undefined !== old_obj.key);
var c = key.localeCompare(old_obj.key);
if (0 === c) break; // already exists
if (c < 0) {
array.removeAt(i); // remove as no longer exists
continue;
}
}
// insert
if (typeof value === 'object') {
// wrap object so we can give it a key
value = Ember.ObjectProxy.create({
content: value,
isProxy: true,
key: key
});
array.insertAt(i, value);
} else {
// wrap raw value so we can give it a key and observe when it changes
value = DataValueObserver.create({
parent: result,
key: key,
});
array.insertAt(i, value);
}
break;
}
}
// remove any trailing items
while (array.objectAt(i)) array.removeAt(i);
};
var should_display = function() {
return true;
};
// use bind helper to call update_array if the contents of property changes
var child_properties = ["[]"];
var preserve_context = true;
return Ember.Handlebars.bind.call(context, property, options, preserve_context, should_display, update_array, child_properties);
});
Inspired by:
How can I make Ember.js handlebars #each iterate over objects?
http://mozmonkey.com/2014/03/ember-getting-the-index-in-each-loops/
https://github.com/emberjs/ember.js/issues/4365
https://gist.github.com/strathmeyer/1371586
Here's that fiddle again if you missed it:
http://jsfiddle.net/L6axcob8/1/

Why does my backbone collection contain an empty model item?

I have a game model with a scorecards attribute that is a collection. I'm nesting this collection so when I initialize I'm using nestCollection to create the change handlers to keep everything updated and in sync. Whenever I create a new game model, an empty model is added to the scorecards attribute collection but only in memory - what is saved to localstorage is correct. I can't figure out why.
This is my game model definition- Notice the console log statement results:
var Game = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage('datastore'),
defaults: {
name : '',
scorecards: new ScorecardList(),
created : 0
},
initialize : function() {
console.log(this.scorecards); // prints undefined
console.log(this.get('scorecards')); // length is 0 as expected
this.scorecards = nestCollection(this, 'scorecards', new ScorecardList(this.get('scorecards')));
console.log(this.scorecards); // length is 1, with empty element in it
console.log(this.get('scorecards')); // length is 0 as expected
if (this.isNew()) this.set('created', Date.now());
}
});
The nesting code:
function nestCollection(model, attributeName, nestedCollection) {
//setup nested references
for (var i = 0; i < nestedCollection.length; i++) {
model.attributes[attributeName][i] = nestedCollection.at(i).attributes;
}
//create empty arrays if none
nestedCollection.bind('add', function (initiative) {
if (!model.get(attributeName)) {
model.attributes[attributeName] = [];
}
model.get(attributeName).push(initiative.attributes);
});
nestedCollection.bind('remove', function (initiative) {
var updateObj = {};
updateObj[attributeName] = _.without(model.get(attributeName), initiative.attributes);
model.set(updateObj);
});
return nestedCollection;
}
This is the code I use to create a new game:
addGame: function () {
var g = new Game({
name:this.ui.gameName.val()
});
app.gameList.create(g,{wait:true});
//Backbone.history.navigate('game/new/'+ g.id, true);
}
Your problem comes from this piece of code:
new ScorecardList(this.get('scorecards'))
Here you're giving your ScorecardList constructor another collection as argument. This collection happens to be an object. So your collection's constructor will think it's an object you're giving it to create a model.
So basically, this.get('scorecards')) gets cast into a Scorecard (or whatever your model is called), and that's why you have an empty model.
Passing arguments to the constructor for a different purpose than the creation of your collection is a bad idea, you should call a method afterwards.

Reset "insertion sort order" for backbone collection

I am setting and unsetting comparator functions on my backbone collections at runtime, and I would like to know if there is a way to "reset" the collection to its original insertion order. For example...
var Mod = Backbone.Model.extend({});
var Col = Backbone.Collection.extend({ model: Mod });
var col = new Col([
{ name: "andy" },
{ name: "chad" },
{ name: "ashley" },
{ name: "louis" }
]);
col.comparator = function(p1, p2) {
return p1.get('name') < p2.get('name') ? -1
: (p1.get('name') > p2.get('name') ? 1 : 0 );
}
col.sort();
col.comparator = false;
// throws, was hoping this would return
// the collection to insertion order
col.sort();
Please disregard the fact that the comparator function I am setting here could be replaced with the sortBy implementation. This is just a contrived example. It is clear to me from the Backbone source that if you try and sort a collection with no comparator, it will throw:
// ... from BB source ...
sort: function(options) {
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
// ...
Is there any way to return the order of the collection to insertion order?
No, there's none. Though you could easily keep some array that'd keep this information to use it later. Redefine the add method, 'hack' the parse one...
But yeah, sadly, it'd make no sense to have one.
You could save the original in the Collection. Something like:
col.originalComp = col.comparator;
col.comparator = function(){};
...Do Stuff ...
col.comparator = col.originalComp; //Back to original

How to make Backbone.js Collection items Unique?

Say I have these Backbone.js Model:
var Truck = Backbone.Model.extend({});
var truck1 = new Truck();
var truck2 = new Truck();
truck1.set("brand", "Ford");
truck2.set("brand", "Toyota");
truck3.set("brand", "Honda");
truck4.set("brand", "Ford");
Then, let's say we have a Backbone.js Collection:
var TruckList = Backbone.Collection.extend({
model: Truck,
comparator: function(truck) {
return truck.get("brand");
};
});
I'm a car collector, so time to add each car to my collection:
Trucks = new TruckList();
Trucks.add(truck1);
Trucks.add(truck2);
Trucks.add(truck3);
Trucks.add(truck4);
Just focusing on the brand attribute, truck4 is a duplicate of truck1. I can't have duplicates in my Collection. I need my collection to have unique values.
My question is, How do I remove duplicate items from my Backbone.js Collection?
Should I use Underscore.js for this? If so, can someone please provide a working/runnable example of how to do this.
Assume the following:
1.Collection is not sorted
Removal must be done on brand attribute value
Ajax call to populate each instance of a Truck. This means when adding to a collection, you don't have access to the Truck properties.
I would override the add method in your TruckList collection and use underscore to detect duplicates there and reject the duplicate. Something like.
TruckList.prototype.add = function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
// Up to you either return false or throw an exception or silently ignore
// NOTE: DEFAULT functionality of adding duplicate to collection is to IGNORE and RETURN. Returning false here is unexpected. ALSO, this doesn't support the merge: true flag.
// Return result of prototype.add to ensure default functionality of .add is maintained.
return isDupe ? false : Backbone.Collection.prototype.add.call(this, truck);
}
The simplest way to achieve this is to make sure the models you are adding have unique ids. By default Backbone collections will not add models with duplicate ids.
test('Collection should not add duplicate models', 1, function() {
var model1 = {
id: "1234"
};
var model2 = {
id: "1234"
};
this.collection.add([model1, model2]);
equal(1, this.collection.length, "collection length should be one when trying to add two duplicate models");
});
Try this. It uses the any underscore method to detect the potential duplicate and then dumps out if so. Of course, you might want to dress this up with an exception to be more robust:
TruckList.prototype.add = function(newTruck) {
var isDupe = this.any(function(truck) {
return truck.get('brand') === newTruck.get('brand');
}
if (isDupe) return;
Backbone.Collection.prototype.add.call(this, truck);
}
As an aside, I would probably write a function on Truck to do the dupe checking so that the collection doesn't know too much about this condition.
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
},
comparator : function(truck) {
return truck.get("brand");
} });
VassilisB's answer worked great but it will override Backbone Collection's add() behavior. Therefore, errors might come when you try to do this:
var truckList = new TruckList([{brand: 'Ford'}, {brand: 'Toyota'}]);
So, I added a bit of a checking to avoid these errors:
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(trucks) {
// For array
trucks = _.isArray(trucks) ? trucks.slice() : [trucks]; //From backbone code itself
for (i = 0, length = trucks.length; i < length; i++) {
var truck = ((trucks[i] instanceof this.model) ? trucks[i] : new this.model(trucks[i] )); // Create a model if it's a JS object
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
}
},
comparator : function(truck) {
return truck.get("brand");
}});
I'm doing a FileUpload thing with the same issue, and here's how I did it (coffeescript):
File = Backbone.Model.extend
validate: (args) ->
result
if !#collection.isUniqueFile(args)
result = 'File already in list'
result
Files = Backbone.Collection.extend
model: File
isUniqueFile: (file) ->
found
for f in #models
if f.get('name') is file.name
found = f
break
if found
false
else
true
... and that's it. The collection object is automatically referenced in File, and Validation is automatically called when an action is invoked on the collection which in this case is Add.
Underscore.js, a pre-req for backbone.js, provides a function for this: http://documentcloud.github.com/underscore/#uniq
Example:
_.uniq([1,1,1,1,1,2,3,4,5]); // returns [1,2,3,4,5]
Not sure if this is an update to either Backbone or underscore, but the where() function works in Backbone 0.9.2 to do the matching for you:
TruckList.prototype.add = function(truck) {
var matches = this.where({name: truck.get('brand')});
if (matches.length > 0) {
//Up to you either return false or throw an exception or silently ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
}
I would prefer override the add method like this.
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
},
comparator : function(truck) {
return truck.get("brand");
} });
It seems like an elegant solution would be to use _.findWhere so long as you have some unique attribute (brand in your case). _.findWhere will return a match which is a JavaScript object and therefore truthy or undefined which is falsey. This way you can use a single if statement.
var TruckList = Backbone.Collection.extend({
model: Truck,
add: function (truck) {
if (!this.findWhere({ brand: truck.get('brand') })) {
Backbone.Collection.prototype.add.call(this, truck);
}
}
});
Try this...
var TruckList = Backbone.Collection.extend({
model: Truck,
comparator: function(truck) {
return truck.get("brand");
},
wherePartialUnique: function(attrs) {
// this method is really only tolerant of string values. you can't do partial
// matches on arrays, objects, etc. use collection.where for that
if (_.isEmpty(attrs)) return [];
var seen = [];
return this.filter(function(model) {
for (var key in attrs) {
// sometimes keys are empty. that's bad, so let's not include it in a unique result set
// you might want empty keys though, so comment the next line out if you do.
if ( _.isEmpty(model.get(key).trim()) ) return false;
// on to the filtering...
if (model.get(key).toLowerCase().indexOf(attrs[key].toLowerCase()) >= 0) {
if (seen.indexOf( model.get(key) ) >= 0 ) return false;
seen.push(model.get(key));
return true;
} else {
return false;
}
}
return true;
});
}
});
A few things to remember:
this is based on the backbone.collection.where method and unlike that method, it will attempt partial matches on model attributes within a collection. If you don't want that, you'll need to modify it to only match exactly. Just mimic what you see in the original method.
it should be able to accept multiple attribute matches, so if you have model attributes of foo and bar, you should be able to do collection.wherePartialUnique({foo:"you",bar:"dude"}). I have not tested that though. :) I have only ever done one key/value pair.
i also strip out empty model attributes from consideration. I don't care about them, but you might.
this method doesn't require a collection of unique model properties that the comparator depends. It's more like a sql distinct query, but I'm not an sql guy so don't shoot me if that's a bad example :)
your collection is sorted by way of the comparator function, so one of your assumptions about it not being sorted is incorrect.
I believe this also addresses all of your goals:
Collection is not sorted
Removal must be done on brand attribute value
Ajax call to populate each instance of a Truck. This means when adding to a collection, you don't have access to the Truck properties.
I'm really unhappy with the accepted answer to this solution. It contains numerous errors. I've edited the original solution to highlight my concerns, but I am proposing the following solution assuming you're OK dirtying your duplicate's id/cid property:
TruckList.prototype.add = function(truckToAdd, options) {
// Find duplicate truck by brand:
var duplicateTruck = this.find(function(truck){
return truck.get('brand') === truckToAdd.get('brand');
});
// Make truck an actual duplicate by ID:
// TODO: This modifies truckToAdd's ID. This could be expanded to preserve the ID while also taking into consideration any merge: true options.
if(duplicateTruck !== undefined){
if(duplicateTruck.has('id')){
truckToAdd.set('id', duplicateTruck.get('id'), { silent: true });
}
else {
truckToAdd.cid = duplicateTruck.cid;
}
}
// Allow Backbone to handle the duplicate instead of trying to do it manually.
return Backbone.Collection.prototype.add.call(this, truckToAdd, options);
}
The only flaw with this one is that truckToAdd's ID/cid is not preserved. However, this does preserve all of the expected functionality of adding an item to a collection including passing merge: true.
I was not satisfied with the provided answers for several reasons:
Modifying the return value of add is unexpected.
Not supporting { merge: true } is unexpected.
I've provided a solution which I believe to be more robust. This solution clones given models if they have duplicates in the collection, updates the clones' ID to match the duplicates ID, and then passes the list of duplicates and non-duplicates onto the original add method so that it can do its magic. No unintended side-effects as far as I am aware.
add: function (models, options) {
var preparedModels;
if (models instanceof Backbone.Collection) {
preparedModels = models.map(this._prepareModelToAdd.bind(this));
}
else if (_.isArray(models)) {
preparedModels = _.map(models, this._prepareModelToAdd.bind(this));
} else if (!_.isNull(models) && !_.isUndefined(models)) {
preparedModels = this._prepareModelToAdd(models);
} else {
preparedModels = models;
}
// Call the original add method using preparedModels which have updated their IDs to match any existing models.
return Backbone.Collection.prototype.add.call(this, preparedModels, options);
},
// Return a copy of the given model's attributes with the id or cid updated to match any pre-existing model.
// If no existing model is found then this function is a no-op.
// NOTE: _prepareModel is reserved by Backbone and should be avoided.
_prepareModelToAdd: function (model) {
// If an existing model was not found then just use the given reference.
var preparedModel = model;
var existingModel = this._getExistingModel(model);
// If an existing model was found then clone the given reference and update its id.
if (!_.isUndefined(existingModel)) {
preparedModel = this._clone(model);
this._copyId(preparedModel, existingModel);
}
return preparedModel;
},
// Try to find an existing model in the collection based on the given model's brand.
_getExistingModel: function (model) {
var brand = model instanceof Backbone.Model ? model.get('brand') : model.brand;
var existingModel = this._getByBrand(brand);
return existingModel;
},
_getByBrand: function (brand) {
return this.find(function (model) {
return model.get('brand') === brand;
});
},
_clone: function (model) {
// Avoid calling model.clone because re-initializing the model could cause side-effects.
// Avoid calling model.toJSON because the method may have been overidden.
return model instanceof Backbone.Model ? _.clone(model.attributes) : _.clone(model);
},
// Copy the model's id or cid onto attributes to ensure Backbone.Collection.prototype.add treats attributes as a duplicate.
_copyId: function (attributes, model) {
if (model.has('id')) {
attributes.id = model.get('id');
} else {
attributes.cid = model.cid;
}
}

Categories

Resources