I'm trying to understand sort and comparator in Backbone.js. I've seen many answers online but I have been unable to get any of them to work. Working with the jsfiddle of the classic "ToDo" application: https://jsfiddle.net/quovadimus/9z3wnoh2/2/ I've tried changing line 34:
comparator: 'order'
with
comparator: function(model) {
return -model.get('order');
}
or even simply:
comparator: 'title
I'm entering "zzz" for the title of my first todo and "aaa" for the next. I expect either of my modifications to reverse the list order of the todos. But every time it is displayed in the original order. What am I missing? thank you
You correctly understand how comparator
works. However, adjusting the order of the collection is only half the story. The view (AppView
in this case) is still free to present the models in the collection in any order. In the fiddle that you linked, the lines of code that determine the order of presentation are the following:
var AppView = Backbone.View.extend({
// ...
initialize: function() {
// ...
this.listenTo(Todos, "add", this.addOne);
this.listenTo(Todos, "reset", this.addAll);
// ...
},
// ...
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
},
addAll: function() {
Todos.each(this.addOne, this);
},
// ...
});
Basically, this code says the following:
add
event, addOne
method), put the newest todo at the bottom of the list.reset
event, addAll
method), place the todos in the order in which they appear in the collection.The exceptional case with the reset
event never happens in your fiddle, not even when you refresh the app with some todos already in your localStorage
, because the fetch
method calls set
rather than reset
.
To solve this, build your view such that it always respects the order of the collection. This is easier said than done; for this reason, I recommend using a dedicated library for this purpose, such as backbone-fractal or Marionette, both of which offer a CollectionView
for this purpose. However, for the sake of education, here is a basic approach for mirroring the order of a collection in a view.
Before we start, stop rendering views from the outside. You'll see lines like view.render().el
all over the internet, because it makes nice example code, but you shouldn't actually do that in production. The rendering of a view is an internal affair and the view's own concern (unless rendering is really expensive, which you should try to avoid, and the view alone cannot determine whether the time is right to do it). In the majority of cases, a view should render once in its initialize
method and then again when its model triggers the 'change'
event. In other words, add this line to TodoView.initialize
:
this.render();
This may not seem important now, but take my word that it simplifies matters enormously if only each individual view needs to keep track of when to render itself.
Next, you need to separate creating subviews (instances of TodoView
) from placing those subviews. Creating subviews should happen at two types of occasions:
AppView
) is initialized, for each model that happens to already be in the collection at that time.add
event.Placing subviews should also happen at two types of occasions, but they are not exactly the same:
update
event.Since creating and placing the subviews needs to be separated in time, you will have to store the subviews in the meanwhile. This also means that you will need to update the storage when models are removed again.
You can choose different approaches for the storage and this in turn affects how to go about creating and placing the subviews. Below, I demonstrate an approach that keeps the subviews in an object that is indexed by the corresponding model's cid
. Keep in mind that this is example code; the basic principles are correct, but in practice, getting all the details right will be tricky. Using a library like backbone-fractal will save you that trouble.
var AppView = Backbone.View.extend({
// ...
initialize: function() {
// ...
this.resetSubviews().placeSubviews().listenTo(Todos, {
add: this.addSubview,
remove: this.forgetSubview,
update: this.placeSubviews,
reset: this.resetSubviews,
});
// ...
},
// ...
addSubview: function(todo) {
this.subviews[todo.cid] = new TodoView({model: todo});
},
forgetSubview: function(todo) {
delete this.subviews[todo.cid];
},
placeSubviews: function() {
var model2el = _.compose(_.property('el'), _.propertyOf(this.subviews), _.property('cid'));
var subviewEls = Todos.map(model2el);
this.$('#todo-list').append(subviewEls);
return this;
},
resetSubview: function() {
this.subviews = {};
Todos.each(this.addSubview, this);
return this;
},
// ...
});