/ ember

Dynamic Sorting in Ember.js

January 7, 2019: This article was originally published on the now-defunct CodeSchool.com blog. See the Internet Archive snapshot.

Most modern web applications that display lists or tables of data often offer the user some mechanism for sorting that data dynamically. From the developer’s point of view, this would ideally happen client-side without the need to contact the server and requerying for the resorted data. So today I want to explore how this can be accomplished in an Ember app.

In this blog post, I’ll be assuming you’re using Ember CLI >= 2.3.0. If you’d like to follow along with the tutorial, I’ve created a base project you can check out here, or you can view the completed tutorial code here.

The Scenario

Let’s imagine we’re working on a product review site. We’ve been given the task of creating a component to display the reviews for a given product and allow for them to be sorted by date, rating, or the name of the reviewer.

We open up the Review model and it looks like this:

// app/models/review.js

import DS from 'ember-data';

export default DS.Model.extend({
  date: DS.attr('date'),
  product: DS.belongsTo('product'),
  rating: DS.attr('number'), // score out of 5
  text: DS.attr('string'),
  title: DS.attr('string'),
  username: DS.attr('string'),
});

We see that each review contains a reference to a product, a title, some text, a rating, as well as the date it was written and the username of the user who wrote it. Let’s get started!

The Basics

First, let’s generate the component scaffolding:

ember generate component review-list

Now, since our component will be a list of reviews, let’s update our component’s tag to use the list tag <ul>.

// app/components/review-list.js

import Ember from 'ember';

export default Ember.Component.extend({
  tagName: 'ul',
});

Next, we’ll create a basic template to output the reviews as we received them — no sorting just yet.

{{!-- app/templates/components/review-list.hbs --}}

{{#each reviews as |review|}}
  <li>
    <h3>{{review.title}} - {{review.rating}} out of 5</h3>
    <p>{{review.text}}</p>
    <p>by {{review.username}} on {{review.date}}</p>
  </li>
{{/each}}

Each review is contained inside a list element. It displays a title and the rating out of five, the full text of the review, and a byline. Right now, it’s just pretty simple stuff.

Now, let’s add our component to the Product template:

{{!-- app/templates/product.hbs --}}

<h1>{{model.title}}</h1>

<img src="{{model.image}}" />

<hr />
<h2>Reviews:</h2>
{{review-list reviews=model.reviews}}

If we run:

ember serve

then navigate to http://localhost:4200, we should see a product with a list of reviews in no particular order. Of course that doesn’t make very much sense, so let’s sort the list.

Sorting

First, let’s sort the reviews by date:

// app/components/review-list.js

import Ember from 'ember';

export default Ember.Component.extend({
  tagName: 'ul',
  sortedReviews: Ember.computed.sort('reviews', 'sortDefinition'),
  sortDefinition: ['date'],
});

Ember.computed.sort takes a reference to the collection it will sort as its first argument — in this case, reviews — and a reference to a sort definition as its second. A sort definition is simply a list of properties to sort by. Each property may be followed by :asc or :desc to specify sorting in ascending or descending order. If left off, the property is sorted in ascending order by default, but more on this later. For our purposes, we’re only ever going to sort by one property at a time. And to begin with, we’ll set our sortDefinition to just one property, date. And that’s it — Ember.computed.sort handles the sorting for us! The collection of reviews returned by sortedReviews is now sorted by date. Now we just need to update our template to use the new sortedReviews property instead of reviews:

{{!-- app/templates/components/review-list.hbs --}}

{{!-- ... --}}
{{#each sortedReviews as |review|}}
{{!-- ... --}}

Side Note on the History of Sorting

Previously in Ember 1.x, dynamic sorting was accomplished using SortableMixin. A good amount of material out there still discusses using this mixin for sorting collections. However, in Ember 2, SortableMixin has been removed — though, inexplicably it does not appear in the 2.x deprecations list — in favor of Ember.computed.sort. It’s still possible to use SortableMixin in Ember 2.x via the ember-legacy-controllers addon, but in this article we’ll be using the Ember-2.x-preferred Ember.computed.sort.

Choosing Which Property to Sort By

So, we’ve changed the reviews to be sorted by their date property. But we want to give the user the ability to choose what property to sort on — by either date, rating, or the reviewer’s username.

We’ll add some logic to our component to handle this:

// app/components/review-list.js

// ...
sortedReviews: Ember.computed.sort('reviews', 'sortDefinition'),
sortBy: 'date', // default sort by date
sortDefinition: Ember.computed('sortBy', function() {
  return [ this.get('sortBy') ];
}),
// ...

First, we create a new property in our component called sortBy and, in keeping consistent with the previous iteration, we’ll set the default value to 'date'. Then we update our sortDefinition to be a computed property — not a sorted computed property, just your run-of-the-mill Ember.computed property. We give it the name of the component property, sortBy, to observe for changes and a callback to return the sortBy property in an array. By default, sortDefinition will return the same value as it did before — ['date']. But now we can change the sortBy value to dynamically update the property the collection of reviews is sorted by.

Let’s give the user the ability to update the value of sortBy. I think this is a perfect job for a set of radio buttons — one each for sorting by date, rating, and username. Ember doesn’t come with a built-in radio button input helper, but some fellow Emberists have already created a nice addon for us. Let’s install it so we can use it in our template:

ember install ember-radio-button
{{!-- app/templates/components/review-list.hbs --}}

<div>
  Sort by
  {{radio-button value='date' groupValue=sortBy}} Date
  {{radio-button value='rating' groupValue=sortBy}} Rating
  {{radio-button value='username' groupValue=sortBy}} User
</div>
{{!-- ... --}}

Here, we create a set of radio buttons, each with one of the Review properties we want to sort by as its value. When a radio button element is clicked, the radio-button helper sets the groupValue equal to the element’s value. So for instance, clicking on the Rating radio button will set the sortBy property to 'rating'. You should see that the Date radio button is selected by default, and that clicking on the others changes the sorting of the reviews. Now there’s just one more thing to do — allow the user to reverse the sort order.

Choosing a Sort Order

I mentioned before that a sort definition can contain a :asc or :desc flag to denote whether the sort should be done in standard ascending order or the reverse, descending order. Let’s give the user the ability to choose this sort order.

// app/components/review-list.js

// ...
sortedReviews: Ember.computed.sort('reviews', 'sortDefinition'),
sortBy: 'date', // default sort by date
reverseSort: false, // default sort in ascending order
sortDefinition: Ember.computed('sortBy', 'reverseSort', function() {
  let sortOrder = this.get('reverseSort') ? 'desc' : 'asc';
  return [ `${this.get('sortBy')}:${sortOrder}` ];
}),
// ...

We add a new static property named reverseSort and give it a value of false so that by default, the items are sorted in ascending order. Then we update our sortDefinition computed property to also observe reverseSort. In the callback, we set the sortOrder flag to either 'desc' if reverseSort is true or 'asc' if it is false, then tack that flag onto the sortBy string, separated by a colon. If you were to log the sortDefinition to the console with default values of sortBy and reverseSort, it would return ['date:asc'].

Now let’s update our template to put the sort ordering choice in the hands of our users.

{{!-- app/templates/components/review-list.hbs --}}

{{!-- ... --}}
<div>
  {{input type='checkbox' checked=reverseSort}} Reverse sort
</div>
{{!-- ... --}}

By default, the Reverse sort checkbox will be unchecked, but when the user checks it, the order of the items will be reversed.

You should now be able to manipulate not only the property the reviews are sorted by, but also whether or not they are shown in ascending or descending order!

Even More Sorting

As I hinted at before, Ember.computed.sort can handle a multivariate sort definition that contains multiple properties by which to sort, each in either ascending or descending order. So even if you have some very specific sorting needs, Ember.computed.sort should be able to handle them.

That said, don’t try to reinvent the wheel. The great thing about the Ember community is that there are so many awesome addons out there. For instance, if you’re looking to sort, filter, and search a data table based on multiple criteria, give the ember-models-table addon a try. In general, EmberObserver.com is a great place to find just about any kind of Ember addon you can imagine. Have your own favorite addons? Let me know in the comments section below!

If you would like an Ember refresher, Code School’s Ember.js course, Try Ember, is a great place to start.