Cleanest way to destroy every Model in a Collection in Backbone?
I'm a bit late here, but I think this is a pretty succinct solution, too:
_.invoke(this.collection.toArray(), 'destroy');
I recently ran into this problem as well. It looks like you resolved it, but I think a more detailed explanation might also be useful for others that are wondering exactly why this is occurring.
So what's really happening?
Suppose we have a collection (library) of models (books).
For example:
console.log(library.models); // [object, object, object, object]
Now, lets go through and delete all the books using your initial approach:
library.each(function(model) {
model.destroy();
});
each
is an underscore method that's mixed into the Backbone collection. It uses the collections reference to its models (library.models
) as a default argument for these various underscore collection methods. Okay, sure. That sounds reasonable.
Now, when model calls destroy
, it triggers a "destroy" event on the collection as well, which will then remove its reference to the model. Inside remove
, you'll notice this:
this.models.splice(index, 1);
If you're not familiar with splice
, see the doc. If you are, you can might see why this is problematic.
Just to demonstrate:
var list = [1,2];
list.splice(0,1); // list is now [2]
This will then cause the each
loop to skip elements because the its reference to the model objects via models
is being modified dynamically!
Now, if you're using JavaScript < 1.6 then you may run into this error:
Uncaught TypeError: Cannot call method 'destroy' of undefined
This is because in the underscore implementation of each
, it falls back on its own implementation if the native forEach
is missing. It complains if you delete an element mid-iteration because it still tries to access non-existent elements.
If the native forEach
did exist, then it would be used instead and you would not get an error at all!
Why? According to the doc:
If existing elements of the array are changed, or deleted, their value as passed to callback will be the value at the time forEach visits them; elements that are deleted are not visited.
So what's the solution?
Don't use collection.each
if you're deleting models from the collection. Use a method that will allow you to work on a new array containing the references to the models. One way is to use the underscore clone
method.
_.each(_.clone(collection.models), function(model) {
model.destroy();
});
You could also use a good, ol'-fashioned pop destroy-in-place:
var model;
while (model = this.collection.first()) {
model.destroy();
}