Note that there are some explanatory texts on larger screens.

plurals
  1. POSorting Backbone Collections
    primarykey
    data
    text
    <h2>The desired functionality</h2> <p>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.</p> <p>Eventually I want to be able to sort numerical fields as well strings. Some strings should sort alphabetically, others by a custom predefined order.</p> <h2>My current approach</h2> <p>I've found the following post on reverse sorting strings: <a href="https://stackoverflow.com/questions/5636812/sorting-strings-in-reverse-order-with-backbone-js">Sorting strings in reverse order with backbone.js</a></p> <p>I tried to combine this with a comparator function and the following custom properties:</p> <ol> <li>a custom sort order for the possible values in the 'status' field</li> <li>which field should be used for sorting</li> <li>which direction to sort</li> </ol> <h2>The Collection and Model classes I've got so far:</h2> <h3>The Collection</h3> <pre><code>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.comparator', this.comparator); var models = _.sortBy(this.models, this.comparator); if (this.sortOrder != 'asc') { models.reverse(); } return models; }, comparator: function(application) { console.log('ApplicationCollection.comparator', 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; } } }); </code></pre> <h3>The Model</h3> <pre><code>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' }); </code></pre> <h2>The current (undesired) result</h2> <p>I've got an instance of the collection stored in 'pendingApplications' like so:</p> <pre><code>var pendingApplications = new ApplicationCollection(); pendingApplications.fetch(); </code></pre> <p>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.</p> <p>To sort the collection I do the following:</p> <pre><code>pendingApplications.sortOrder = 'asc'; pendingApplications.sortField = 'current_timestamp'; // format: YYYY-mm-dd HH:mm:ss pendingApplications.sort(); </code></pre> <p>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.</p> <p>However, the 'ApplicationCollection.comparator' method executes within the global scope and I'm not sure why. Also none of the arguments on the comparator method contain the 'pendingApplications' instance.</p> <p>What I would like is for my comparator 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.</p> <h2>Scope issue? Wrong approach? Any suggestions are welcome...</h2> <p>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?</p> <h1>The Solution</h1> <p>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.</p> <pre><code>/** * 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.comparator = 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 comparator 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); } // compare that shizzle! if (valA &gt; valB) { return (direction == 'desc') ? -1 : 1; } if (valA &lt; valB) { return (direction == 'desc') ? 1 : -1; } else { if (a.get('id') &gt; b.get('id')) { return (direction == 'desc') ? -1 : 1; } else if (a.get('id') &lt; 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; } </code></pre> <p>Usage of the decorator:</p> <pre><code>original = new ApplicationsCollection(); sortable = SortedCollection(original); sortable.sortCriteria = { sortField: 'submission_timestamp', sortDirection: 'desc' } sortable.sort(); </code></pre> <p>The above snippet does the following:</p> <ul> <li>Instantiates a new ApplicationsCollection;</li> <li>Instantiates a Sortable Collection that extends the original and listens to relevant events on the original collection.</li> <li>Tells the Sortable Collection to sort by 'submission_timestamp' property in descending order.</li> <li>sorts the Sortable Collection.</li> </ul> <p>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.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload