Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cleanest way to destroy every Model in a Collection in Backbone?

Tags:

backbone.js

On the first attempt I wrote

this.collection.each(function(element){
    element.destroy();
});

This does not work, because it's similar to ConcurrentModificationException in Java where every other elements are removed.

I tried binding "remove" event at the model to destroy itself as suggested Destroying a Backbone Model in a Collection in one step?, but this will fire 2 delete requests if I call destroy on a model that belongs to a collection.

I looked at underscore doc and can't see a each() variant that loops backwards, which would solve the removing every element problem.

What would you suggest as the cleanest way to destroy a collection of models?

Thanks

like image 342
Henry Avatar asked Jun 02 '12 00:06

Henry


4 Answers

You could also use a good, ol'-fashioned pop destroy-in-place:

var model;

while (model = this.collection.first()) {
  model.destroy();
}
like image 137
rjz Avatar answered Oct 22 '22 17:10

rjz


I'm a bit late here, but I think this is a pretty succinct solution, too:

_.invoke(this.collection.toArray(), 'destroy');
like image 13
Sean Anderson Avatar answered Oct 22 '22 16:10

Sean Anderson


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();
});
like image 25
linstantnoodles Avatar answered Oct 22 '22 16:10

linstantnoodles


Piggybacking on Sean Anderson answer. There is a direct access to backbone collection array, so you could do it like this.

_.invoke(this.collection.models, 'destroy');

Or just call reset() on the collection with no parameters, destroy metod on the models in that collection will bi triggered.

this.collection.reset(); 

http://backbonejs.org/#Collection-models

like image 1
Ivan V. Avatar answered Oct 22 '22 16:10

Ivan V.