Proper way to sort a backbone.js collection on the fly

41,351

Solution 1

Interesting question. I would try a variant on the strategy pattern here. You could create a hash of sorting functions, then set comparator based on the selected member of the hash:

App.SomeCollection = Backbone.Collection.extend({
    comparator: strategies[selectedStrategy],
    strategies: {
        firstName: function () { /* first name sorting implementation here */ }, 
        lastName: function () { /* last name sorting implementation here */ },
    },
    selectedStrategy: "firstName"
});

Then you could change your sorting strategy on the fly by updating the value of the selectedStrategy property.

EDIT: I realized after I went to bed :) that this wouldn't quite work as I wrote it above, because we're passing an object literal to Collection.extend. The comparator property will be evaluated once, when the object is created, so it won't change on the fly unless forced to do so. There is probably a cleaner way to do this, but this demonstrates switching the comparator functions on the fly:

var SomeCollection = Backbone.Collection.extend({
    comparator: function (property) {
        return selectedStrategy.apply(myModel.get(property));
    },
    strategies: {
        firstName: function (person) { return person.get("firstName"); }, 
        lastName: function (person) { return person.get("lastName"); },
    },
    changeSort: function (sortProperty) {
        this.comparator = this.strategies[sortProperty];
    },
    initialize: function () {
        this.changeSort("lastName");
        console.log(this.comparator);
        this.changeSort("firstName");
        console.log(this.comparator);        
    }                                                                                        
});

var myCollection = new SomeCollection;

Here's a jsFiddle that demonstrates this.

The root of all of your problems, I think, is that properties on JavaScript object literals are evaluated immediately when the object is created, so you have to overwrite the property if you want to change it. If you try to write some kind of switching into the property itself it'll get set to an initial value and stay there.

Here's a good blog post that discusses this in a slightly different context.

Solution 2

Change to comparator function by assigning a new function to it and call sort.

// Following example above do in the view:

// Assign new comparator
this.collection.comparator = function( model ) {
  return model.get( 'lastname' );
}

// Resort collection
this.collection.sort();

// Sort differently
this.collection.comparator = function( model ) {
  return model.get( 'age' );
}
this.collection.sort();

Solution 3

So, this was my solution that actually worked.

  App.Collection = Backbone.Collection.extend({

    model:App.Model,

    initialize: function(){
      this.sortVar = 'firstName';
    },

    comparator: function( collection ){
      var that = this;
      return( collection.get( that.sortVar ) );
    }

  });

Then in the view, I have to M-I-C-K-E-Y M-O-U-S-E it like this:

this.collections.sortVar = 'lastVar'

this.collections.sort( this.comparator ).each( function(){
  // All the stuff I want to do with the sorted collection... 
});

Since Josh Earl was the only one to even attempt a solution and he did lead me in the right direction, I accept his answer. Thanks Josh :)

Solution 4

This is an old question but I recently had a similar need (sort a collection based on criteria to be supplied by a user click event) and thought I'd share my solution for others tackling this issue. Requires no hardcoded model.get('attribute').

I basically used Dave Newton's approach to extending native JavaScript arrays, and tailored it to Backbone:

MyCollection = Backbone.Collection.extend({

    // Custom sorting function.
    sortCollection : function(criteria) {

        // Set your comparator function, pass the criteria.
        this.comparator = this.criteriaComparator(criteria);
        this.sort();
    },

    criteriaComparator : function(criteria, overloadParam) {

        return function(a, b) {
            var aSortVal = a.get(criteria);
            var bSortVal = b.get(criteria);

            // Whatever your sorting criteria.
            if (aSortVal < bSortVal) {
                return -1;
            }

            if (aSortVal > bSortVal) {
                return 1;
            }

            else {
                return 0;
            }

        };
    } 

});

Note the "overloadParam". Per the documentation, Backbone uses Underscore's "sortBy" if your comparator function has a single param, and a native JS-style sort if it has two params. We need the latter, hence the "overloadParam".

Solution 5

Looking at the source code, it seems there's a simple way to do it, setting comparator to string instead of function. This works, given Backbone.Collection mycollection:

        mycollection.comparator = key;
        mycollection.sort();
Share:
41,351
kikuchiyo
Author by

kikuchiyo

Ruby on Rails / Javascript Developer and QA-Automation Engineer

Updated on July 09, 2022

Comments

  • kikuchiyo
    kikuchiyo almost 2 years

    I can successfully do this:

    App.SomeCollection = Backbone.Collection.extend({
      comparator: function( collection ){
        return( collection.get( 'lastName' ) );
      }
    });
    

    Which is nice if I want to have a collection that is only sorted by 'lastName'. But I need to have this sorting done dynamically. Sometimes, I'll need to sort by, say, 'firstName' instead.

    My utter failures include:

    I tried passing an extra variable specifying the variable to sort() on. That did not work. I also tried sortBy(), which did not work either. I tried passing my own function to sort(), but this did not work either. Passing a user-defined function to sortBy() only to have the result not have an each method, defeating the point of having a newly sorted backbone collection.

    Can someone provide a practical example of sorting by a variable that is not hard coded into the comparator function? Or any hack you have that works? If not, a working sortBy() call?

  • kikuchiyo
    kikuchiyo about 12 years
    Thank you. Suggested implementation and variants have been unsuccessful, but I am thankful for the response :). I think I'm missing something really small here. Quite frustrating. I'm leaning towards another framework for my javascript work considering how much difficulty I'm having with a simple sort variant >.<
  • Josh Earl
    Josh Earl about 12 years
    Thanks for the answer. :) I updated my original post--see if that works for you.
  • jhorback
    jhorback over 10 years
    This is what I've always done too. I use a mixin on the collection so it is available on all collections. I also trigger a reset on the collection since I usually have views that are bound that need to update as well.
  • Neil Girardi
    Neil Girardi almost 10 years
    I'm about to have a go at solving this problem myself. My app displays a table. The user will sort the table contents by clicking the column names in the tablehead. I'm considering making the tablehead a region with it's own "sort" model. The model will track the current "sortCriteria." Clicking a column name will update the sortCriteria. I will have one comparator function in the collection. It will simply get the sortCriteria from the sort model.
  • Trip
    Trip over 9 years
    Great simple answer. Thanks!
  • Chris Dutrow
    Chris Dutrow over 8 years
    I might be missing something, but this seems like the best solution.
  • Spencer Williams
    Spencer Williams about 8 years
    The JSFiddles need to be updated to use an Underscore and Backbone that exist on the server. The ones at cdnjs.com are good. JSFiddle use to provide their own, but I guess not anymore. My initial assessment was a little harsh, but I think myModel should be replaced with the parameter passed to the comparator, which should be each model in the collection.
  • Spencer Williams
    Spencer Williams about 8 years
    If you happen to be doing the exercise in Udacity's JavaScript Design Patterns where you must dynamically change the sort order of the ToDo example, note that you must call the app.AppView's addAll method to re-add the list of newly sorted items. In addition to re-rendering a view, check how the list is being populated. Here's an example
  • Jyothu
    Jyothu almost 8 years
    But, It doesn't work if we have null values with in the collection.