admin管理员组

文章数量:1296901

The desired functionality

I'm using a Backbone.Collection to view data in a sortable list. I've got the part down where clicking on certain dom elements sets properties on my Collection to determine which field i'd like to sort, and which direction the sort should be done. A sort should then be triggered on the Collection after which the view will update.

Eventually I want to be able to sort numerical fields as well strings. Some strings should sort alphabetically, others by a custom predefined order.

My current approach

I've found the following post on reverse sorting strings: Sorting strings in reverse order with backbone.js

I tried to bine this with a parator function and the following custom properties:

  1. a custom sort order for the possible values in the 'status' field
  2. which field should be used for sorting
  3. which direction to sort

The Collection and Model classes I've got so far:

The Collection

var ApplicationCollection = Backbone.Collection.extend({
  model: ApplicationModel,
  url: 'rest/applications',

  // sorting
  statusOrder: ['new', 'to_pay', 'payed', 'ready'],
  sortField: 'submission_timestamp',
  sortOrder: 'asc',
  sortBy: function() {
    console.log('ApplicationCollection.sortBy', this, arguments);
    console.log('thisparator', thisparator);
    var models = _.sortBy(this.models, thisparator);
    if (this.sortOrder != 'asc') {
      models.reverse();
    }
    return models;
  },
  parator: function(application) {
    console.log('ApplicationCollectionparator', this, arguments);
    switch(this.sortField) {
      case 'status':
        return _.indexOf(this.statusOrder, application.get(this.sortField));
        break;
      case 'submission_timestamp':
      default:
        return application.get(this.sortField)
        break;
    }
  }
});

The Model

var ApplicationModel = Backbone.Model.extend({
  defaults: {
    'drupal_id': 0,
    'language': '',
    'last_name': '',
    'first_name': '',
    'address': '',
    'zip_code': '',
    'city': '',
    'country': '',
    'telephone': '',
    'cell_phone': '',
    'email': '',
    'date_of_birth': '',
    'email': '',
    'date_of_birth': '',
    'pilot_training_institute': '',
    'date_of_exam': '',
    'flight_hours_total': '',
    'date_of_last_flight': '',
    'date_of_mcc_certificate': '',
    'date_of_ir_renewel': '',
    'package': '',
    'submission_timestamp': '',
    'status': 'new'
  },
  urlRoot: 'rest/applications'
});

The current (undesired) result

I've got an instance of the collection stored in 'pendingApplications' like so:

var pendingApplications = new ApplicationCollection();
pendingApplications.fetch();

This loads applications from the server, and all works as planned. I can render a view with a list of applications, all properties are there on the models, etc.

To sort the collection I do the following:

pendingApplications.sortOrder = 'asc';
pendingApplications.sortField = 'current_timestamp'; // format: YYYY-mm-dd HH:mm:ss
pendingApplications.sort();

This triggers the sort function on the Collection. The console tells me the 'ApplicationCollection.sortBy' method executes within the scope of the 'pendingApplications' instance, which is as expected.

However, the 'ApplicationCollectionparator' method executes within the global scope and I'm not sure why. Also none of the arguments on the parator method contain the 'pendingApplications' instance.

What I would like is for my parator method to be executed within the scope of the 'pendingApplications' instance instead, or at least I'd like to be able to somehow be able to access properties on the 'pendingApplications' instance.

Scope issue? Wrong approach? Any suggestions are wele...

Does anyone know how I can resolve this issue? Or am I going about this the wrong way and is there another solution to arbitrarily defining custom sorts on a Backbone.Collection?

The Solution

I ended up implementing a sorting functionality as decorator for the Backbone.Collection. The reason to do it this way is because I've also got a decorator for filtering items in a collection. By using a sorting decorator I can apply sorting to the filtered sub-set of items which is potentially faster.

/**
 * returns a new Backbone.Collection which represents a sorted version of the
 * data contained within the original Backbone.Collection.
 *
 * @param {Backbone.Collection} original
 */
SortedCollection: function(original, criteria) {
  var sorted = new original.constructor(),

  // sensible defaults
  defaultSortCriteria = {
    custom: {},
    field: 'id',
    direction: 'asc'
  };

  // configuration
  sorted.sortCriteria = _.extend(defaultSortCriteria, criteria);

  // do the stuff
  sortedparator = function(a, b) {
    // @formatter:off
    var criteria = this.sortCriteria,
        custom, 
        field = criteria.field,
        direction = criteria.direction,
        valA,
        valB;
        // @formatter:on

    // custom sort
    if (_.has(criteria.custom, field)) {
      custom = criteria.custom[field];

      // custom param is a parator itself.
      if (_.isFunction(custom)) {
        return custom(a, b);
      }
      // custom param is an example of a sorted array.
      else if (_.isArray(custom)) {
        valA = _.indexOf(custom, a.get(field));
        valB = _.indexOf(custom, b.get(field));
      }
      else {
        throw new Error('Invalid custom sorting criterium.');
      }
    }
    // nothing custom here, use the field value directly.
    else {
      valA = a.get(field);
      valB = b.get(field);
    }

    // pare that shizzle!
    if (valA > valB) {
      return (direction == 'desc') ? -1 : 1;
    }
    if (valA < valB) {
      return (direction == 'desc') ? 1 : -1;
    }
    else {
      if (a.get('id') > b.get('id')) {
        return (direction == 'desc') ? -1 : 1;
      }
      else if (a.get('id') < b.get('id')) {
        return (direction == 'desc') ? 1 : -1;
      }
      else {
        return 0;
      }
    }
  };

  // update collection if original changes
  original.on("add", function(model) {
    sorted.add(model);
  });
  original.on("reset", function() {
    sorted.reset(original.models);
  });
  original.on("remove", function(model) {
    sorted.remove(model);
  });

  return sorted;
}

Usage of the decorator:

original = new ApplicationsCollection();
sortable = SortedCollection(original);
sortable.sortCriteria = { 
  sortField: 'submission_timestamp',
  sortDirection: 'desc'
}
sortable.sort();

The above snippet does the following:

  • Instantiates a new ApplicationsCollection;
  • Instantiates a Sortable Collection that extends the original and listens to relevant events on the original collection.
  • Tells the Sortable Collection to sort by 'submission_timestamp' property in descending order.
  • sorts the Sortable Collection.

The new Sortable Collection also stays sorted automatically when new models are added to or removed from the original Collection, or when the original Collection is reset.

The desired functionality

I'm using a Backbone.Collection to view data in a sortable list. I've got the part down where clicking on certain dom elements sets properties on my Collection to determine which field i'd like to sort, and which direction the sort should be done. A sort should then be triggered on the Collection after which the view will update.

Eventually I want to be able to sort numerical fields as well strings. Some strings should sort alphabetically, others by a custom predefined order.

My current approach

I've found the following post on reverse sorting strings: Sorting strings in reverse order with backbone.js

I tried to bine this with a parator function and the following custom properties:

  1. a custom sort order for the possible values in the 'status' field
  2. which field should be used for sorting
  3. which direction to sort

The Collection and Model classes I've got so far:

The Collection

var ApplicationCollection = Backbone.Collection.extend({
  model: ApplicationModel,
  url: 'rest/applications',

  // sorting
  statusOrder: ['new', 'to_pay', 'payed', 'ready'],
  sortField: 'submission_timestamp',
  sortOrder: 'asc',
  sortBy: function() {
    console.log('ApplicationCollection.sortBy', this, arguments);
    console.log('this.parator', this.parator);
    var models = _.sortBy(this.models, this.parator);
    if (this.sortOrder != 'asc') {
      models.reverse();
    }
    return models;
  },
  parator: function(application) {
    console.log('ApplicationCollection.parator', this, arguments);
    switch(this.sortField) {
      case 'status':
        return _.indexOf(this.statusOrder, application.get(this.sortField));
        break;
      case 'submission_timestamp':
      default:
        return application.get(this.sortField)
        break;
    }
  }
});

The Model

var ApplicationModel = Backbone.Model.extend({
  defaults: {
    'drupal_id': 0,
    'language': '',
    'last_name': '',
    'first_name': '',
    'address': '',
    'zip_code': '',
    'city': '',
    'country': '',
    'telephone': '',
    'cell_phone': '',
    'email': '',
    'date_of_birth': '',
    'email': '',
    'date_of_birth': '',
    'pilot_training_institute': '',
    'date_of_exam': '',
    'flight_hours_total': '',
    'date_of_last_flight': '',
    'date_of_mcc_certificate': '',
    'date_of_ir_renewel': '',
    'package': '',
    'submission_timestamp': '',
    'status': 'new'
  },
  urlRoot: 'rest/applications'
});

The current (undesired) result

I've got an instance of the collection stored in 'pendingApplications' like so:

var pendingApplications = new ApplicationCollection();
pendingApplications.fetch();

This loads applications from the server, and all works as planned. I can render a view with a list of applications, all properties are there on the models, etc.

To sort the collection I do the following:

pendingApplications.sortOrder = 'asc';
pendingApplications.sortField = 'current_timestamp'; // format: YYYY-mm-dd HH:mm:ss
pendingApplications.sort();

This triggers the sort function on the Collection. The console tells me the 'ApplicationCollection.sortBy' method executes within the scope of the 'pendingApplications' instance, which is as expected.

However, the 'ApplicationCollection.parator' method executes within the global scope and I'm not sure why. Also none of the arguments on the parator method contain the 'pendingApplications' instance.

What I would like is for my parator method to be executed within the scope of the 'pendingApplications' instance instead, or at least I'd like to be able to somehow be able to access properties on the 'pendingApplications' instance.

Scope issue? Wrong approach? Any suggestions are wele...

Does anyone know how I can resolve this issue? Or am I going about this the wrong way and is there another solution to arbitrarily defining custom sorts on a Backbone.Collection?

The Solution

I ended up implementing a sorting functionality as decorator for the Backbone.Collection. The reason to do it this way is because I've also got a decorator for filtering items in a collection. By using a sorting decorator I can apply sorting to the filtered sub-set of items which is potentially faster.

/**
 * returns a new Backbone.Collection which represents a sorted version of the
 * data contained within the original Backbone.Collection.
 *
 * @param {Backbone.Collection} original
 */
SortedCollection: function(original, criteria) {
  var sorted = new original.constructor(),

  // sensible defaults
  defaultSortCriteria = {
    custom: {},
    field: 'id',
    direction: 'asc'
  };

  // configuration
  sorted.sortCriteria = _.extend(defaultSortCriteria, criteria);

  // do the stuff
  sorted.parator = function(a, b) {
    // @formatter:off
    var criteria = this.sortCriteria,
        custom, 
        field = criteria.field,
        direction = criteria.direction,
        valA,
        valB;
        // @formatter:on

    // custom sort
    if (_.has(criteria.custom, field)) {
      custom = criteria.custom[field];

      // custom param is a parator itself.
      if (_.isFunction(custom)) {
        return custom(a, b);
      }
      // custom param is an example of a sorted array.
      else if (_.isArray(custom)) {
        valA = _.indexOf(custom, a.get(field));
        valB = _.indexOf(custom, b.get(field));
      }
      else {
        throw new Error('Invalid custom sorting criterium.');
      }
    }
    // nothing custom here, use the field value directly.
    else {
      valA = a.get(field);
      valB = b.get(field);
    }

    // pare that shizzle!
    if (valA > valB) {
      return (direction == 'desc') ? -1 : 1;
    }
    if (valA < valB) {
      return (direction == 'desc') ? 1 : -1;
    }
    else {
      if (a.get('id') > b.get('id')) {
        return (direction == 'desc') ? -1 : 1;
      }
      else if (a.get('id') < b.get('id')) {
        return (direction == 'desc') ? 1 : -1;
      }
      else {
        return 0;
      }
    }
  };

  // update collection if original changes
  original.on("add", function(model) {
    sorted.add(model);
  });
  original.on("reset", function() {
    sorted.reset(original.models);
  });
  original.on("remove", function(model) {
    sorted.remove(model);
  });

  return sorted;
}

Usage of the decorator:

original = new ApplicationsCollection();
sortable = SortedCollection(original);
sortable.sortCriteria = { 
  sortField: 'submission_timestamp',
  sortDirection: 'desc'
}
sortable.sort();

The above snippet does the following:

  • Instantiates a new ApplicationsCollection;
  • Instantiates a Sortable Collection that extends the original and listens to relevant events on the original collection.
  • Tells the Sortable Collection to sort by 'submission_timestamp' property in descending order.
  • sorts the Sortable Collection.

The new Sortable Collection also stays sorted automatically when new models are added to or removed from the original Collection, or when the original Collection is reset.

Share edited May 23, 2017 at 12:03 CommunityBot 11 silver badge asked Aug 3, 2012 at 18:54 Ruben VreekenRuben Vreeken 9761 gold badge13 silver badges22 bronze badges
Add a ment  | 

2 Answers 2

Reset to default 7

The parator() function is called in the scope of the collection by default, at least in the most current version of Backbone.

I suspect that you may have broken that by defining the sortBy() function. That function is already defined by Backbone, and is used internally by Backbone's sort() function in some cases. Try removing that function and see if it works as expected.

It appears that you're just using sortBy() to reverse the order of your sort. That can be acplished in the parator() function by multiplying your return value by -1 when appropriate.

As per the other answer, modify you sortBy method to call the original Collection.sortBy method like this:

sortBy: function(){
  var models = Backbone.Collection.prototype.sortBy.apply(this, arguments);
  if (this.sortOrder != 'asc') {
    models.reverse();
  }
  return models;
}

本文标签: javascriptSorting Backbone CollectionsStack Overflow