Last Updated: February 25, 2016
·
5.011K
· technotronicoz

Multi-Sort A Backbone Collection

The Collection

var MultiSortCollection = Backbone.Collection.extend({

    /**
     * Sort by supplied attributes.  First param is sorted first, and
     * last param is final subsort
     * @param {String} sortAttributes
         * @example collection.sortBy("last_name","first_name")
     */
    sortBy : function(sortAttributes){
        var attributes = arguments;
                if(attributes.length){
                    this.models = this._sortBy(this.models,attributes);
                }   
    },

    /**
     * Recursive sort
     */
    _sortBy : function(models,attributes){
        var attr,
                that = this;
        //base case
        if(attributes.length === 1){
            attr = attributes[0];
            return _(models).sortBy(function(model){
                return model.get(attr);
            });
        }
        else{
            attr = attributes[0];
            attributes = _.last(attributes,attributes.length-1);

            //split up models by sort attribute, 
            //then call _sortBy with remaining attributes
            models = _(models).chain().
                sortBy(function(model){
                    return model.get(attr);
                }).
                groupBy(function(model){
                    return model.get(attr);
                }).
                toArray().
                value();

            _(models).each(function(modelSet,index){
                models[index] = that._sortBy(models[index],attributes);
            });
            return _(models).flatten(); 
        }
    }
});

The Model

var models = new MultiSortCollection,
model;

models.add([
    {name : "Charlie",number: 5},
    {name : "Billy", number: 7},
    {name : "Albert",number: 1},
    {name : "Charlie",number: 4}
]);

//collection order is [Charlie 5, Billy 7, Albert 1, Charlie 4]

models.sortBy("number","name");  //collection order is now [Albert 1, Charlie 4, Charlie 5, Billy 7]
models.sortBy("name","number");  //collection order is now [Albert 1, Billy 7, Charlie 4, Charlie 5]

model = new Backbone.Model({name : "Charlie",number:4.5});
console.log(models.sortIndex(model));       //returns 3
models.add(model);  //colleciton order is now [Albert 1, Billy 7, Charlie 4, Charlie 4.5, Charlie 5]

1 Response
Add your response

Hello, pretty handy code, except that your use of Underscore's groupBy() method once you _.chain() all the models actually destroys the sort. It took me a while to figure out why, but it has to do with the fact that an object's properties cannot be returned in a guaranteed sort order when you call a for (prop in object) loop. That for (prop in object) loop happens when you call toArray() on the object returned by groupBy(), which means your top-level sort will never be correct. The second and tertiary sorts will be correct, just not the top-level sort order.

When you call groupBy() on the array of models (which were already sorted), the array is converted to an object with properties for each model (which were previously in an array, which has a permanent order), and this object immediately loses your sort order. The solution I came up with was to remove the chaining and create my own array-grouping code:<br/><br/>
models = models.sortBy(function(model){<br/>   [code used to sort the collection]<br/> });<br/> var modelsArray = []; // new array to receive our arrays<br/>var subArray = []; // subarray used for grouping of similar data items<br/> for (var i = 1, l = models.length; i < l; i++) { // start the loop at index 1, because we only push the prev model<br/>  if (models[i - 1][sortField] === models[i][sortField]) { // does the prev value match the current value?<br/>    subArray.push(models[i - 1]); // push the prev model into subArray bc it is part of a group<br/> } else {<br/>    if (subArray.length > 0) { // were we previously putting models in the subArray?<br/>      subArray.push(models[i - 1]); // push the last model into the subArray<br/>      modelsArray.push(subArray); // and push the subArray into the top-level array<br/>      subArray = []; // reset the subArray so we know what's going on<br/> } else { // only push to the top-level array bc there is no grouping<br/>    modelsArray.push([models[i - 1]]);<br/>    }<br/>  }<br/> }<br/> if (subArray.length > 0) { // be sure to push the last model!<br/>  subArray.push(models[models.length - 1]); // last model is part of a group, so push it to the subArray<br/>  modelsArray.push(subArray); // push subArray to top-level array<br/> } else {<br/>  modelsArray.push([models[models.length - 1]]); // push the last model to the top-level array<br/> }<br/> </code><br/><br/> Also, in the recursive _.each() call, I recommend checking to see of the current array's length is > 1, because there is no need to call a recursive sort on an array with one item:<br/><br/> _.each(modelsArray, function (modelSet, index) { // for each group of models, resort<br/>  if (modelSet.length > 1) { // no need to sort arrays with one value<br/>    modelsArray[index] = that._sortBy(models[index], attributes); // recursively call the _sortBy function with each group of models and remaining search terms<br/>  }<br/> });<br/> return _.flatten(modelsArray); // combine all the arrays and subArrays into one<br/> </code><br/><br/> Aside from the fact that it doesn't actually work as written, your solution is pretty good. Thanks for providing the jumping-off point I used to get to the final code.

over 1 year ago ·