backbone.js

Understanding Sort and comparator in Backbone.js


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


Solution

  • 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:

    1. In the normal case (add event, addOne method), put the newest todo at the bottom of the list.
    2. In an exceptional case (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:

    1. When the parent view (AppView) is initialized, for each model that happens to already be in the collection at that time.
    2. Each time after that when a new model is added to the collection, i.e., on the add event.

    Placing subviews should also happen at two types of occasions, but they are not exactly the same:

    1. When the parent view is initialized, for each model that happens to be already in the collection at that time.
    2. Each time after that when the collection gains new models or changes order, i.e., on the 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;
            },
            // ...
        });