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
You could also use a good, ol'-fashioned pop destroy-in-place:
var model;
while (model = this.collection.first()) {
model.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();
});
I'm a bit late here, but I think this is a pretty succinct solution, too:
_.invoke(this.collection.toArray(), 'destroy');
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
This works, kind of surprised that I can't use underscore for this.
for (var i = this.collection.length - 1; i >= 0; i--)
this.collection.at(i).destroy();
I prefer this method, especially if you need to call destroy on each model, clear the collection, and not call the DELETE to the server. Removing the id or whatever idAttribute is set to is what allows that.
var myCollection = new Backbone.Collection();
var models = myCollection.remove(myCollection.models);
_.each(models, function(model) {
model.set('id', null); // hack to ensure no DELETE is sent to server
model.destroy();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone-min.js"></script>
You don't need underscore and for loop for this.
this.collection.slice().forEach(element => element.destroy());
Related
I have a collection to show.
1. for each model in collection, create a View
2. append view.render().el
I find view.render() takes long, more specifically
this.$el.html(this.template(data)) part.
I need to speed things up and found 'DOM manipulation' is slow.
So I looked for the ways to batch process the rendering but didn't find much.
Question 1.
I wonder if there is a way to batch process and attach the final html to the DOM without attaching each row to the DOM?
(I suspect this.$el.html() does the DOM manipulation. If so, can I somehow not perform the this.$el.html() call in view.render() and later assign the view's el to decrease the DOM interaction?)
Question 2.
Are there other pitfalls or performance blocker when redering views in clients?
edit
addAll: function() {
this.$('#thread-loop').html('');
var fragment = document.createDocumentFragment();
for (var i = 0; i < this.threads.length; i++) {
console.log('adding one');
var thread = this.threads.at(i);
var View = this.threadTypeToViewMap[thread.get('thread_type')];
var view = new View( {model: thread, forumSelector: this.forumSelector} );
fragment.appendChild(view.render().el);
}
this.$el.find('#thread-loop')[0].appendChild(fragment);
// this.threads.each(this.addOne, this);
},
Actually, I realised I'm using the fragment technique.
I nailed down the problem more and it looks like when javascript object has lengthy property (user created data), handlebar takes long to find the property (or so I suspect) in template() call.
Take a look at http://marionettejs.com
They extend Backbone to provide views that are optimized for collections (so you can create the DOM nodes in a loop and only add them to the document after you are done). This is going to really help you with performance.
As for question 2, the things that will give you pitfalls are:
Extensive DOM manipulation
Compiling templates on the client instead of the server (i.e. making individual requests for each template after loading your page)
General JavaScript performance bottlenecks (e.g. using polyfill foreach loops using jQuery or underscore instead of native JS, etc.)
Backbone template renders are slow because by default, they use JavaScript's with structure to scope the variable names properly as you expect. A little-known feature of Backbone templates allows you to skip this rather large performance penalty by assigning a variable to put all your data in (typically just "data"):
var t = _.template('<%= data.x %>', null, {variable: 'data'});
template({x: 42});
instead of the normal:
_.template('<%= x %>');
template({x: 42});
Do this, and you will see huge performance gains. This will probably solve your performance issues immediately.
Benchmark: http://jsperf.com/underscore-template-function-with-variable-setting
For Q1 ->
If your $el is not in the dom already, you can append all the $el's into a Document Fragment and then inject the Document Fragment into the page at once. JQuery's John Resig has written a nice writeup about how document fragments work : http://ejohn.org/blog/dom-documentfragments
A couple of things you could try on top of using the doc fragment:
1- If you want to use the for loop, cache the length prior to looping. var threadLength = this.threads.length;
2- That said, I would opt for using _.each instead of the for loop.
3- if you do 2, the threads model should be easily accessed. I don't know if there is any hit to using get(), but you could just access the model.attributes.thread_type directly.
3- Is your template cached?
4- Why are you changing the html of #thread-loop twice? Is that needed? If you need to access it twice, then cache that selector also.
I currently started using Firebase as my backend solution for persistance.
I found it easy to create new objects and persist it to Firebase with Backfire with a simple
collection.add(obj)
The issue comes when I try to get a Firebase collection from the server.
For example when i try
console.log(collection);
I get this output:
=> {length: 0, models: Array[0], _byId: Object, _events: Object, constructor: function…}
Which result in an empty models array
console.log(collection.models);
=> []
After some searching, I figured out that Backbone Collections aren't yet loaded at the time I try to log it to the console (see this previous question).
I also tried using
Backbone.Collection.extend({
model: Todo,
firebase: new Backbone.Firebase("https://<your-namespace>.firebaseio.com")
});
To explicitly call fetch from the server and use success callback with no success either.
My question is: How can I get the Firebase Collection and populate the DOM from it?
When you call Backbone.Firebase.Collection.add, it does not get added to the collection synchronously. Rather, it sends the request to Firebase and then waits for the return event. See the code here
Thus, if you immediately try to read the collection, you will see zero elements. However, if you try something like this:
collection.once('add', function() { console.log(collection.length); });
You'll see the element you have added.
Remember that we're dealing with real-time data here, so when you want to populate the DOM, you shouldn't think in terms of a single transaction, but instead rely on events and take everything as you get it (in real time).
So to populate the DOM, do something like this in your view:
Backbone.View.extend({
render: function() {
this.listenTo(this.collection, 'add', this.rowAdded);
},
rowAdded: function(m) {
/* use `m` here to create your new DOM element */
}
});
Additionally, you'll probably want to check out a nice binding library like ModelBinder to help you deal with the constantly changing DOM, so you don't have to re-invent any wheels.
It seems you have to use a Backbone.Firebase.Collection and not a Backbone.Collection which will tell you that your calls to fetch or sync are silently ignored.
Also, Backbone.Firebase's got a read and a readall methods that should get you started. It seems Backbone.Firebase.Collection doesn't inherit this method, but I'm not sure though.
Edit:
As Kato stated in his comment, it seems you don't have to do anything. Just use Backbone.Backfire.Collection and Backbone.Backfire.Model.
I'm attempting to learn backbone.js and (by extension) underscore.js, and I'm having some difficulty understanding some of the conventions. While writing a simpel search filter, I thought that something like below would work:
var search_string = new RegExp(query, "i");
var results = _.filter(this, function(data){
return search_string.test(data.get("title"));
}));
But, in fact, for this to work I need to change my filter function to the following:
var search_string = new RegExp(query, "i");
var results = _(this.filter(function(data){
return search_string.test(data.get("title"));
}));
Basically, I want to understand why the second example works, while the first doesn't. Based on the documentation (http://documentcloud.github.com/underscore/#filter) I thought that the former would have worked. Or maybe this just reflects some old jQuery habits of mine... Can anyone explain this for me?
I'd guess that you're using a browser with a native Array#filter implementation. Try these in your console and see what happens:
[].filter.call({ a: 'b' }, function(x) { console.log(x) });
[].filter.call([1, 2], function(x) { console.log(x) });
The first one won't do anything, the second will produce 1 and 2 as output (http://jsfiddle.net/ambiguous/tkRQ3/). The problem isn't that data is empty, the problem is that the native Array#filter doesn't know what to do when applied to non-Array object.
All of Underscore's methods (including filter) use the native implementations if available:
Delegates to the native filter method, if it exists.
So the Array-ish Underscore methods generally won't work as _.m(collection, ...) unless you're using a browser that doesn't provide native implementations.
A Backbone collection is a wrapper for an array of models, the models array is in c.models so you'd want to:
_.filter(this.models, function(data) { ... });
Backbone collections have several Underscore methods mixed in:
Backbone proxies to Underscore.js to provide 28 iteration functions on Backbone.Collection.
and one of those is filter. These proxies apply the Underscore method to the collection's model array so c.filter(...) is the same as _.filter(c.models, ...).
This mixing-in is probably what's confusing the "should I use the native method" checks that Underscore is doing:
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
You can use _.filter on a plain old object (_.filter({a:'b'}, ...)) and get sensible results but it fails when you _.filter(backbone_collection, ...) because collections already have Underscore methods.
Here's a simple demo to hopefully clarify things: http://jsfiddle.net/ambiguous/FHd3Y/1/
For the same reason that $('#element') works and $#element doesn't. _ is the global variable for the underscore object just like $ is the global variable for the jQuery object.
_() says look in the _ object. _filter says look for a method named _filter.
I'm using a Signalr hub to subscribe to events on the server. What an event is dispatched to a hub, its successfully adding the item to a Marionette CollectionView. This, in turn, is rendered to a table.
Because the table of events is essentially a blotter, I'd like the events in reverse order and preferably only keep n-number of events.
Can Backbone 'automatically' re-render a collection in reverse order?
To go through collection in the reverse order I usually use a construction like this:
_.each(collection.last(collection.length).reverse(), function(model){ });
There is a thread on this topic at https://github.com/marionettejs/backbone.marionette/issues/78
Although Backbone keeps the collection sorted once you define a comparator, as #breischl pointed out, Marionette does not automatically re-render the CollectionView when order changes. In fact, Marionette listens to the add event on the collection and appends a new ItemView.
If you want your CollectionView to always display items in reverse chronological order, and you want new items added to be prepended instead of appended, then override the appendHtml method in your CollectionView as follows:
var MyCollectionView = Backbone.Marionette.CollectionView.extend({
appendHtml: function(collectionView, itemView){
collectionView.$el.prepend(itemView.el);
}
});
If you want to be able to insert at a particular location as #dira mentioned in the comment, there is a solution posted at the link above on github by sberryman that I reproduce here for convenience (disclaimer: I haven't tested the code below personally):
Change appendHtml to:
appendHtml: function(collectionView, itemView) {
var itemIndex;
itemIndex = collectionView.collection.indexOf(itemView.model);
return collectionView.$el.insertAt(itemIndex, itemView.$el);
}
And add the following to extend jQuery to provide insertAt function:
(function($) {
return jQuery.fn.insertAt = function(index, element) {
var lastIndex;
if (index <= 0) return this.prepend(element);
lastIndex = this.children().size();
if (index >= lastIndex) return this.append(element);
return $(this.children()[index - 1]).after(element);
};
})(jQuery);
Usually you'll have the rendering take place in your Backbone.View 'subclass'. So you have something like:
render: function() {
this.collection.each( function(model) {
// some rendering of each element
}, this );
}
this.collection is presumably a Backbone.Collection subclass, and so you can just use underscore.js methods on it to get it in whatever order you like:
this.collection.reverse().each( ... )
this.collection.sort( function(m) { ... } ).each( ... )
Etc.
Of course, you are getting a single element from your backend, and you want to insert it in the right place without re-rendering the whole thing! So in that case just go old school and insert your sort key as a rel attribute or data attribute on the elements, and use that to insertAfter or similar with jQuery in your renderNewItem (or similar) method.
Backbone automatically keeps Collections in sorted order. If you want to use a non-default sort, define a comparator() function on your Collection and it will use that instead. The comparator can take either one or two arguments, see the Backbone documentation for details.
You can then render your collection in an .each() loop, and it will come out in the correct order. Adding new items to the view in sorted order is up to you, though.
From what you describe, you don't need to re-render the collection in reverse order. Just add an event for add on your collection in that view and have it call a function that renders the item just added and prepends it to the table.
this.collection.on('add', this.addItem);
You can reverse your models in a collection like so...
this.collection.models = this.collection.models.reverse()
If you use lodash instead of underscore you can also do this:
_(view.collection.models).reverse();
As the BackBone doesn't support reverse iteration of the collection (and it's just waste of resources to reverse or worse sort the collection) the easiest and fastest approach is to use the for loop with decreasing index over models in the collection.
for (var i = collection.length - 1; i >= 0; i--) {
var model = collection.models[i];
// your logic
}
It's not that elegant as sorting or reversing the collection using Underscore but the performace is much better. Try to compare different loops here just to know what costs you to write foreach instead of classic for.
Here's the Example
I was following this excellent tutorial by Thomas Davis : What is a model?
Somehow the 'change' binding is not firing. What am I doing wrong here?
Backbone is checking if the set value is the same as the previous value (look at https://github.com/documentcloud/backbone/blob/master/backbone.js#L210 and on).
In your example, the array is still the same but the value inside changed. This is tricky to solve. Creating a new copy of the array seems to be overhead. I would suggest to call the change event directly in your adopt function as a solution:
adopt: function(newChildsName){
var children_array = this.get('children');
children_array.push(newChildsName);
this.set({children:children_array});
this.trigger("change:children");
}
I would suggest to create an issue on backbone github repository to maybe add a "force" option to force the update (thus triggering the event) of attributes on a model.
Here is a bit awkward solution:
adopt: function(newChildsName){
var children_array = this.get('children').splice(0);
children_array.push(newChildsName);
this.set({children:children_array});
}
Instead of using children as an plain array we can use it as an collection and listen to the add,remove events of the collection.