estella

Sort lists with Ember ArrayController

Ember makes sorting lists easy with ArrayController. The first time I used it for sorting, it took longer than it should've to implement due the lack of solid examples. I hope this post helps others out there. This uses custom sorting to organize a list of blog posts.

If you'd rather skip this post, the final working example can be viewed here. The code is available on GitHub.

A simple ArrayController

Let's build a simple Ember.ArrayController called App.IndexController and bind it to a list of posts:

App.IndexController = Ember.ArrayController.extend({
    contentBinding: Ember.Binding.oneWay('App.Post.FIXTURES')
  });

App.Posts.FIXTURES is a static collection of blog posts:

App.Post.FIXTURES = [
  App.Post.create({
    id: 1,
    title: 'A New CSS Bubble',
    content: 'Nullam enim justo, pretium sed nisl eu, vulputate placerat elit. In ultrices nisi dui, ut viverra nunc condimentum nec. Sed posuere, mi vel iaculis commodo, risus tortor scelerisque ligula, vel consectetur metus orci non dolor. Vestibulum odio dolor, tristique eu congue id, mollis quis eros. Morbi vulputate tincidunt vulputate.',
    published: 'Wed May 01 2013 05:30:00 GMT-0700 (PDT)',
    labels: ['css']
  }),
  App.Post.create({
    id: 2,
    title: 'The Best Javascript Best Practices',
    content: 'Integer ornare ut arcu in condimentum. Phasellus ut dui dictum, mollis ipsum in, elementum mauris. Etiam a viverra leo. Suspendisse nec rutrum ligula. Quisque in libero urna. Vestibulum vel lacus dolor. Morbi commodo fringilla elit, eget elementum ligula vestibulum non. Nunc et venenatis ipsum.',
    published: 'Sun Jun 02 2013 15:53:31 GMT-0700 (PDT)',
    labels: ['javascript']
  }),
  App.Post.create({
    id: 3,
    title: 'CSS Preprocessors',
    content: '
Phasellus sollicitudin, nunc vel suscipit convallis, justo sapien rhoncus purus, in scelerisque enim magna quis lectus. Pellentesque ut condimentum ante, sit amet semper mi. Sed ut molestie libero. Aliquam ornare sagittis arcu, eget varius lacus ultricies suscipit.',
    published: 'Sun Jun 09 2013 15:53:31 GMT-0700 (PDT)',
    labels: ['css']
  })
];

App.Post itself is a simple Ember.Object:

App.Post = Ember.Object.extend({
  title: null,
  content: null,
  published: null,
  labels: []
});

Use the helper to display each blog post:

<script type="text/x-handlebars" data-template-name="index">
  <ul>
    
      <li>
        <h3>{{title}}</h3>
        <time {{bindAttr datetime="published"}}>
          {{published}}
        </time>
        {{{content}}}
      </li>
    
  </ul>
</script>

This gives us a list that looks roughly like:

View the online example on GitHub.

Sorting the content

Per the Ember documentation, sortProperties specifies which properties dictate the arrangedContent's sort order; sortAscending specifies the arrangedContent's sort order. Both are part of the Ember.SortableMixin.

Note that these properties affect arrangedContent and not the ArrayController's content property. arrangedContent is the same as content, except it's the list that gets manipulated and sorted.

Let's update our ArrayController with values for these two properties. By default, we'll order by the published date in descending order. We want users to be able to order by title as well, so we'll include that property.

App.IndexController = Ember.ArrayController.extend({
  contentBinding: Ember.Binding.oneWay('App.Post.FIXTURES'),

  /**
  @property sortProperties
  Properties dictating the arrangedContent's sort order.
  */
  sortProperties: ['published', 'title'],

  /**
  @property sortAscending
  The arrangedContent's sort direction.
  */
  sortAscending: false
});

Our list should now resemble this, with the most recent posts at the top:

View the online example on GitHub.

Let's add the ability for users to change the order of the posts. We'll do this with a simple select box, utilizing the Ember.Select view:

<p>
  <label>Sort by:
    
    </label>
</p>

We've bound the content to sortOptions, which we'll define in ourApp.IndexController:

/**
@property sortOptions
The list of options to display inside an Ember.Select view
*/
sortOptions: [
  Ember.Object.create({
    label: "published (ascending)",
    sortAscending: true,
    value: "published-asc",
    property: "published"
  }),
  Ember.Object.create({
    label: "published (descending)",
    sortAscending: false,
    value: "published-desc",
    property: "published"
  }),
  Ember.Object.create({
    label: "title (ascending)",
    sortAscending: true,
    value: "title-asc",
    property: "title"
  }),
  Ember.Object.create({
    label: "title (descending)",
    sortAscending: false,
    value: "title-desc",
    property: "title"
  })
],

/**
@property currentSortOption
The currently selected sort option
*/
currentSortOption: null,

init: function() {
  if (!this.get('currentSortOption')) {
    // Set the default sort option to
    // "published (descending)"
    this.set('currentSortOption', this.get('sortOptions')[1]);
  }
  return this._super();
}

Also included is a currentSortOption to keep track of the currently selected option. When the init event fires, we'll set this to the default. When overriding built-in methods, don't forget to call this._super(), otherwise strange things may happen as Ember may be unable to do important setup work.

This alone won't work though. In order to trigger sorting on the blog posts, we need to update sortProperties and sortAscending. So let's add an observer for currentSortOption—whenever it changes we'll update sortProperties and sortAscending.

/**
@method
Fires whenever currentSortOption changes. Updates sortAscending and sortProperties to trigger reording
*/
currentSortOptionChanged: function() {
  var sortOption = this.get('currentSortOption');

  // Update the sortAscending property
  this.set('sortAscending', sortOption.get('sortAscending'));

  // Update the sortProperties array
  var propertyName = sortOption.get('property'),
      // Clone the sortProperties array
      sortProperties = this.get('sortProperties').slice();

  // Update sortProperties if it's changed
  if (sortProperties[0] !== propertyName) {
    // Remove this property from the array
    sortProperties.splice(sortProperties.indexOf(propertyName), 1);
    // ... and add it back in at the beginning
    sortProperties.unshift(propertyName);
    // Update sortProperties
    this.set('sortProperties', sortProperties);
  }
}.observes('currentSortOption')

And voilà! We have a working example for sorting with Ember.

View the online example on GitHub.

But… notice there are two issues:

  1. The dates are being treated as strings because that's how they're set up in the model.
  2. The title starting with "The" is pushed to the bottom, but what if we want to ignore stop words?

Custom sorting

Whenever the sort properties change in an ArrayController, the orderBy method is triggered to reorder the list. Let's override this method in App.IndexController to convert dates to true dates and remove stop words from titles:

/**
@method
Called by Ember.SortableMixin to sort the arrangedContent collection
*/
orderBy: function(item1, item2) {
  var self = this,
      result = 0,
      sortProperties = this.get('sortProperties'),
      sortAscending = this.get('sortAscending'),
      val1, val2;

  sortProperties.forEach(function(propertyName) {
    if (result === 0) {
      val1 = item1.get(propertyName);
      val2 = item2.get(propertyName);

      switch (propertyName) {
        case 'title':
          val1 = self.cleanString(val1);
          val2 = self.cleanString(val2);
          break;
        // It's best to ensure our model has the
        // correct data types, but we'll include this
        // here just for an example.
        case 'published':
          // Convert each date to a true date
          val1 = new Date(val1);
          val2 = new Date(val2);
          break;
      }

      result = Ember.compare(val1, val2);
      if ((result !== 0) && !sortAscending) {
        result = (-1) * result;
      }
    }
  });

  return result;
},

/**
@method cleanString
Removes stop words from the string
@param {String} str: the string to clean
*/
cleanString: function(str) {
  // Convert to lowercase so uppercase & lowercase
  // letters are weighted evenly
  var s = str.toLowerCase(),
      stopWords = ['a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'for', 'if', 'in', 'into', 'is', 'it', 'no', 'not', 'on', 'or', 'such', 'that', 'the', 'their', 'then', 'there', 'these', 'they', 'this', 'to', 'was', 'will', 'with'],
      re = new RegExp('(' + stopWords.join('\\b\\s?|') + '\\b\\s?)', 'gm');
  // Remove the noise words
  s = s.replace(re, '').trim();
  return s;
}

Our list is now ordered properly.

View the final working example on GitHub.