ChatGPT解决这个技术问题 Extra ChatGPT

Reverse sort order with Backbone.js

With Backbone.js I've got a collection set up with a comparator function. It's nicely sorting the models, but I'd like to reverse the order.

How can I sort the models in descending order rather than ascending?

For reverse sorting on Strings (including the created_at fields), see this answer on another question
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/…
This is what worked for me (Strings and Numbers): stackoverflow.com/questions/5013819/…

I
Infeligo

Well, you can return negative values from comparator. If we take, for example, the example from Backbone's site and want to reverse the order, it will look like this:

var Chapter  = Backbone.Model;
var chapters = new Backbone.Collection;

chapters.comparator = function(chapter) {
  return -chapter.get("page"); // Note the minus!
};

chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));

alert(chapters.pluck('title'));

hi5 for using same example code as mine !, and within 15 seconds difference. Like minded i must say.
Oh, of course, just a minus sign. Thank you both for your quick responses!
Also note that (as mentioned on Backbone's site) this breaks if you change any of the model's attributes (if you add a "length" attribute to chapter for instance). In that case you will have to manually call "sort()" on the Collection.
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/…
@Fasani response works for types other than numbers, please have a look: stackoverflow.com/questions/5013819/…
A
Andrew Newdigate

Personally, I'm not that happy with any of the solutions given here:

The multiplying by -1 solution fails to work when the sort type is non-numeric. Although there are ways around this (by calling Date.getTime() for example) these cases are specific. I would like a general way to reverse the direction of any sort, without having to worry about the specific type of the field being sorted.

For the string solution, constructing a string one character at a time seems like a performance bottleneck, especially when using large collections (or indeed, large sort field strings)

Here's a solution that works nicely for String, Date and Numeric fields:

Firstly, declare a comparator that will reverse the result of a result of a sortBy function, like this:

function reverseSortBy(sortByFunction) {
  return function(left, right) {
    var l = sortByFunction(left);
    var r = sortByFunction(right);

    if (l === void 0) return -1;
    if (r === void 0) return 1;

    return l < r ? 1 : l > r ? -1 : 0;
  };
}

Now, if you want to reverse the direction of the sort, all we need to do is:

var Chapter  = Backbone.Model;
var chapters = new Backbone.Collection;

// Your normal sortBy function
chapters.comparator = function(chapter) {
  return chapter.get("title"); 
};


// If you want to reverse the direction of the sort, apply 
// the reverseSortBy function.
// Assuming the reverse flag is kept in a boolean var called reverseDirection 
if(reverseDirection) {
   chapters.comparator = reverseSortBy(chapters.comparator);
}

chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));

alert(chapters.pluck('title'));

The reason this works is that Backbone.Collection.sort behaves differently if the sort function has two arguments. In this case, it behaves in the same manner as the comparator passed into Array.sort. This solution works by adapting the single argument sortBy function into a two argument comparator and reversing the result.


As the person with the most upvoted answer to this question, I really like this approach and think it should have more upvotes.
Very nice, I ended up tweaking this a bit and abstracting it in Backbone.Collection's prototype. Thanks a bunch!
A
Andrew De Andrade

Backbone.js's collection comparator relies on the Underscore.js method _.sortBy. The way sortBy is implemented ends up "wrapping" up javascript .sort() in a way that makes sorting strings in reverse difficult. Simple negation of the string ends up returning NaN and breaks the sort.

If you need to perform a reverse sort with Strings, such as reverse alphabetical sort, here's a really hackish way of doing it:

comparator: function (Model) {
  var str = Model.get("name");
  str = str.toLowerCase();
  str = str.split("");
  str = _.map(str, function(letter) { 
    return String.fromCharCode(-(letter.charCodeAt(0)));
  });
  return str;
}

It's by no means pretty, but it is a "string negator". If you don't have any qualms with modifying native object types in javascript, you could make you code clearer by extracting the string negator and adding it as a method on String.Prototype. However you probably only want to do this if you know what you are doing, because modifying native object types in javascript can have unforeseen consequences.


This is very handy. Thanks Andrew!
No prob. I would just add that you should ask yourself why you are implementing a reverse alphabetical sort and if there is perhaps a more elegant solution from a user experience perspective.
@AndrewDeAndrade On why: I want to sort a collection by the 'created_at' attribute, serving up the newest ones first. This attribute is one of Backbone's default ones, and is stored as String. Your answer comes closest to what I want. Thanks!
This is amazing, been working on a solution to this for the past hour now.
T
Tomasz Trela

My solution was to reverse results after sort.

To prevent double rendering first sort with silent, then trigger 'reset'.

collections.sort({silent:true})
collections.models = collections.models.reverse();
collections.trigger('reset', collections, {});

A better solution is to negate the result of the comparator.
Daniel, that isn't always possible, for instance when sorting strings or Dates.
Sure it is. For Date you can have the comparator return -Date.getTime(), possibly with some extra hacking if you believe dates in your system will cross the unix epoch. For alphabetical, look at Andrew's answer above.
@DanielXMoore Why is that necessarily a better solution? Is it faster? Personally I find it very easy to store the attribute that the user last sorted the collection by, then reverse the current sort if he again sorts by that attribute; (to implement the sorting behavoir you see in a Wikipedia table, where sort-direction can be toggled by repeatedly clicking the attribute in the table-header) That way i keep my comparator logic clean and save myself a sorting operation if we switch from Asc to Desc sort order
D
Daniel X Moore

Modify your comparator function to return some reversely proporitional value instead of returning the data that you are currently. Some code from : http://documentcloud.github.com/backbone/#Collection-comparator

Example:

var Chapter  = Backbone.Model;
var chapters = new Backbone.Collection;

/* Method 1: This sorts by page number */
chapters.comparator = function(chapter) {
  return chapter.get("page");
};

/* Method 2: This sorts by page number in reverse */
chapters.comparator = function(chapter) {
  return -chapter.get("page");
};

chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));

r
rbhitchcock

The following worked well for me:

comparator: function(a, b) {
  // Optional call if you want case insensitive
  name1 = a.get('name').toLowerCase();
  name2 = b.get('name').toLowerCase();

  if name1 < name2
    ret = -1;
  else if name1 > name2
    ret = 1;
  else
    ret = 0;

  if this.sort_dir === "desc"
    ret = -ret
  return ret;
}

collection.sort_dir = "asc";
collection.sort(); // returns collection in ascending order
collection.sort_dir = "desc";
collection.sort(); // returns collection in descending order

F
Fasani

I read some where that the Backbone comparator function either uses or mimics the JavaScript Array.prototype.sort() function. This means it uses either 1, -1 or 0 to decide on the sort order of each model in the collection. This constantly updates and the collection stays in the correct order based on the comparator.

Info on Array.prototype.sort() can be found here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FArray%2Fsort

Many answers on this question simply reverse the order just before rending it to the page. This is not ideal.

Using the above article on Mozilla as a guide I was able to keep my collection sorted in reverse order using the following code, then we simply render the collection to page in it's current order and we can use a flag (reverseSortDirection) for reversing the order.

//Assuming this or similar will be in your Backbone.Collection.
sortKey: 'id', //The field name of the item in your collection

reverseSortDirection: false,

comparator: function(a, b) {

  var sampleDataA = a.get(this.sortKey),
      sampleDataB = b.get(this.sortKey);

      if (this.reverseSortDirection) {
        if (sampleDataA > sampleDataB) { return -1; }
        if (sampleDataB > sampleDataA) { return 1; }
        return 0;
      } else {
        if (sampleDataA < sampleDataB) { return -1; }
        if (sampleDataB < sampleDataA) { return 1; }
        return 0;
      }

},

Comparator except's two models, on changes to the collection or model the compartor function runs and changes the order of the collection.

I have tested this approach with Numbers and Strings and it seems to be working perfectly for me.

I really hope this answer can help as I have struggled with this problem many times and usually end up using a work around.


I like this because the logic lives in the collection, but you can easily set sortKey and reverseSortDirection and call .sort() from any view that has a handle on the collection. Simpler than other higher-voted answers, to me.
A
Aman Mahajan

This can be done elegantly by overriding sortBy method. Here is an example

var SortedCollection = Backbone.Collection.extend({

   initialize: function () {
       // Default sort field and direction
       this.sortField = "name";
       this.sortDirection = "ASC";
   },

   setSortField: function (field, direction) {
       this.sortField = field;
       this.sortDirection = direction;
   },

   comparator: function (m) {
       return m.get(this.sortField);
   },

   // Overriding sortBy (copied from underscore and just swapping left and right for reverse sort)
   sortBy: function (iterator, context) {
       var obj = this.models,
           direction = this.sortDirection;

       return _.pluck(_.map(obj, function (value, index, list) {
           return {
               value: value,
               index: index,
               criteria: iterator.call(context, value, index, list)
           };
       }).sort(function (left, right) {
           // swap a and b for reverse sort
           var a = direction === "ASC" ? left.criteria : right.criteria,
               b = direction === "ASC" ? right.criteria : left.criteria;

           if (a !== b) {
               if (a > b || a === void 0) return 1;
               if (a < b || b === void 0) return -1;
           }
           return left.index < right.index ? -1 : 1;
       }), 'value');
   }

});

So you can use it like this:

var collection = new SortedCollection([
  { name: "Ida", age: 26 },
  { name: "Tim", age: 5 },
  { name: "Rob", age: 55 }
]);

//sort by "age" asc
collection.setSortField("age", "ASC");
collection.sort();

//sort by "age" desc
collection.setSortField("age", "DESC");
collection.sort();

This solution does not depend on the field type.


Good one ! "void 0" can be replaced by "undefined".
w
wprl
// For numbers, dates, and other number-like things
var things = new Backbone.Collection;
things.comparator = function (a, b) { return b - a };

// For strings
things.comparator = function (a, b) {
  if (a === b) return 0;
  return a < b ? -1 : 1;
};

d
dezman

Here is a really simple solution, for those who simply want to flip the current order. It is useful if you have a table whose columns can be ordered in two directions.

collection.models = collection.models.reverse()

You can combine it with something like this if you are interested in the table case (coffeescript) :

  collection.comparator = (model) ->
    model.get(sortByType)

  collection.sort()

  if reverseOrder
    collection.models = collection.models.reverse()

In this case, when a new model is added to the collection it will not be sorted correctly since the .set method keep new added model's in order by using the sort method, get the result from before the reverseOrder is applied.
m
mzzl

For reverse order on id:

comparator: function(object) { return (this.length - object.id) + 1; }

J
Josh Mc

I had a slightly different requirement, in that I am displaying my models in a table, and I wanted to be able to sort them by any collumn (model property) and either ascending or descending.

It has taken a fair amount of mucking around to get all the cases working (where values can be strings, empty strings, dates, numbers. However I think this is pretty close.

    inverseString: function(str)
    {
        return str.split("").map(function (letter) { return String.fromCharCode(-(letter.charCodeAt(0))); }).join("");
    }
    getComparisonValue: function (val, desc)
    {
        var v = 0;
        // Is a string
        if (typeof val === "string")
        {
            // Is an empty string, upgrade to a space to avoid strange ordering
            if(val.length === 0)
            {
                val = " ";
                return desc ? this.inverseString(val) : val;
            }                

            // Is a (string) representing a number
            v = Number(val);
            if (!isNaN(v))
            {
                return desc ? -1 * v : v;
            }
            // Is a (string) representing a date
            v = Date.parse(val);
            if (!isNaN(v))
            {
                return desc ? -1 * v : v;
            }
            // Is just a string
            return desc ? this.inverseString(val) : val;
        }
        // Not a string
        else
        {
            return desc ? -1 * val : val;
        }
    },

use:

    comparator: function (item)
    {
        // Store the collumn to 'sortby' in my global model
        var property = app.collections.global.get("sortby");

        // Store the direction of the sorting also in the global model
        var desc = app.collections.global.get("direction") === "DESC";

        // perform a comparison based on property & direction
        return this.getComparisonValue(item.get(property), desc);
    },

u
user2686693

I just overrode the sort function to reverse the models array upon completion. Also I held off triggering the 'sort' event until after the reverse has been completed.

    sort : function(options){

        options = options || {};

        Backbone.Collection.prototype.sort.call(this, {silent:true});
        this.models.reverse();

        if (!options.silent){
            this.trigger('sort', this, options);
        }

        return this;
    },

D
Diego Alejos

Recently ran into this issue and just decided to add an rsort method.

    Collection = Backbone.Collection.extend({
            comparator:function(a, b){
                return a>b;
            },
            rsort:function(){
                var comparator = this.comparator;

                this.comparator = function(){
                    return -comparator.apply(this, arguments);
                }
                this.sort();

                this.comparator = comparator;

            }
    });

Hopefully someone will find this useful


J
Johnny Javascript

Here's a quick and easy way to reverse-sort a collection's models using UnderscoreJS

var reverseCollection = _.sortBy(this.models, function(model) { return self.indexOf(model) * -1; });