Backbone.js collection comparator sort by multiple fields?

25,147

Solution 1

@amchang87's answer definitely works, but another that I found worked is simply returning an array of the sortable fields:

this.col = Backbone.Collection.extend({
    model: M,
    comparator: function(item) {
      return [item.get("level"), item.get("title")]
    }
});

I haven't tested this in multiple browsers yet as I think it relies on JS' behavior in sort order for arrays (based on their contents). It definitely works in WebKit.

Solution 2

String concatenation works fine when sorting multiple fields in ascending order, but it didn't work for me because 1) I had to support asc/desc per field and 2) certain fields were number field (i.e., I want 10 to come after 2 if it is ascending). So, below was a comparator function I used and worked OK for my needs. It assumes the backbone collection has a variable assigned with 'sortConfig', which is an array of JSON objects with field name and sort order direction. For example,

{
    "sort" : [
        {
            "field": "strField",
            "order": "asc"
         },
         {
             "field": "numField",
             "order": "desc"
         },
         ...
     ]
}

With the JSON object above assigned as 'sortConfig' to the collection, the function below will make Backbone sort by strField in ascending order first, then sort by numField in descending order, etc. If no sort order is specified, it sorts ascending by default.

multiFieldComparator: function(one, another) {
    // 'this' here is Backbone Collection
    if (this.sortConfig) {
        for (var i = 0; i < this.sortConfig.length; i++) {
            if (one.get(this.sortConfig[i].field) > another.get(this.sortConfig[i].field)) {
                return ("desc" != this.sortConfig[i].order) ? 1 : -1;
            } else if (one.get(this.sortConfig[i].field) == another.get(this.sortConfig[i].field)) {
                // do nothing but let the loop move further for next layer comparison
            } else {
                return ("desc" != this.sortConfig[i].order) ? -1 : 1;
            }
        }
    }
    // if we exited out of loop without prematurely returning, the 2 items being
    // compared are identical in terms of sortConfig, so return 0
    // Or, if it didn't get into the if block due to no 'sortConfig', return 0
    // and let the original order not change.
    return 0;
}

Solution 3

Returning an array is not consistent if you need to sort descending and some ascending...

I created a small set of functions which can be used to return the relevant comparison integer back to Backbone Comparator function:

backbone-collection-multisort

Solution 4

The main thing is that Backbone sorts by a single relative value of one item to another. So it's not directly possible to sort twice in a single collection but I'd try this.

this.col = Backbone.Collection.extend({
    model: M,
    comparator: function(item) {
      // make sure this returns a string!
      return item.get("level") + item.get("title");
    }
});

What this will do is return a string of like "1Cool", "1title", "2newTitle" ... Javascript should sort the strings by the numerical character first then each character afterwards. But this will only work as long as your levels have the same amount of digits. IE "001title" vs "200title". The main idea though is that you need to produce two comparable objects, line a number or string, that can be compared to each other based on one criteria.

Other solution would be to use underscore to "groupby" your level then use "sortby" to manually sort each level group then manually replace the underlying collection with this newly created array. You can probably setup a function to do this whenever the collection "changes".

Share:
25,147
Harry
Author by

Harry

Updated on July 09, 2022

Comments

  • Harry
    Harry almost 2 years
      this.col = Backbone.Collection.extend({
        model: M,
        comparator: function(item) {
          return item.get("level");
        }
      });
    

    This above code sorts items by level. I want to sort by level, then by title. Can I do that? Thanks.

  • Harry
    Harry over 12 years
    If I'm understanding the 'groupby' method that would mess up my data modeling wouldn't it? The first solution is kinda nice though, so thanks for that.
  • geon
    geon over 11 years
    There is support for sort-style comparators since the beginning of 2012. Just accept 2 arguments and return -1, 0 or 1. github.com/documentcloud/backbone/commit/…
  • vhs
    vhs about 10 years
    So straight-forward, this technique is. Here's an example in CoffeeScript: gist.github.com/jhabdas/9822535
  • Algy Taylor
    Algy Taylor over 9 years
    Not sure this is the best way to do it. For example, if you had 2 models: model1-> level: 1, title: "1-thing"; model2 -> level: 10, title: "-thing", then you would expect it to be sorted model1, model2 - but by that comparator, it would be comparing string "11-thing" and "10-thing", so would be sorted in the wrong order
  • RobW
    RobW almost 9 years
    Note that (at least in Webkit) the two fields will be compared as strings. If you're trying to do number sorting you're going to end up with 1, 10, 2, 20, 200, etc.
  • mu is too short
    mu is too short about 8 years
    @RobW: Not quite, it will actually end up comparing the stringified arrays rather than their elements so this answer is incorrect and will only work by accident.
  • mu is too short
    mu is too short about 8 years
    This is functional identical to trying to sort by an array, returning [1,'x'] from a single argument comparator function will sort by the string "1,x" instead of comparing the elements.
  • Jyothu
    Jyothu about 8 years
    @RobW You are correct. How can I achieve the same, If i have 2 numeric fields rather than text fields ?
  • RobW
    RobW about 8 years
    @Jyothu Make a custom comparator that turns the strings into numbers using parseInt() and then compares them. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…