AngularJS Filter for Ordering Objects (Associative Arrays or Hashes) with ngRepeat

I ran into an issue today using the ngRepeat directive in AngularJS. ngRepeat let’s you iterate over a collection (array or object) and repeat a snippet of code for each of the items. Let’s look at a some examples first, before I get into my situation.

Iterating an Array Strings

Let’s assume we have an array of items set in our controller’s scope.

$scope.items = ['red', 'green', 'blue']

We can loop through these items using ngRepeat in the controller’s template.

<ul>
  <li ng-repeat="item in items">{{ item }}</li>
</ul>

Iterating an Array of Objects

Ok, now let’s get slightly more advanced. Let’s say we have an array of objects with a color field.

$scope.items = [{ color: 'red' }, { color: 'green' }, { color: 'blue' }]

Again, we can use ngRepeat to loop through them. This time we’ll incorporate a built-in filter to sort by that color field.

<ul>
  <li ng-repeat="item in items | orderBy:'color'">{{ item.color }}</li>
</ul>

Easy enough. The orderBy filter uses the color property to sort the objects before iterating through them.

Iterating an Object of Objects (Acting as an Associative Array or Hash)

Finally, we’ll look at the situation I ran into. Instead of $score.items being an array, it’s going to be an object used as an associative array (or hash). So we have something like this:

$scope.items = {}
$scope.items['color-1'] = { color: 'red' }
$scope.items['color-2'] = { color: 'green' }
$scope.items['color-3'] = { color: 'blue' }

This may look the same as the example above, but it’s not. $score.items is no longer an array of objects. It is an object itself (acting like an associative array or hash) with the properties color-1, color-2, and color-3. We need to iterate through those properties. ngRepeat does have the ability to do this.

<ul>
  <li ng-repeat="(key, item) in items | orderBy:'color'">{{ item.color }}</li>
</ul>

However, the built-in orderBy filter will no longer work when iterating an object. It’s ignored due to the way that object fields are stored. This was the issue I ran into. And here is my solution… a custom filter:

yourApp.filter('orderObjectBy', function () {
  return function (items, field, reverse) {
    var filtered = []
    angular.forEach(items, function (item) {
      filtered.push(item)
    })
    filtered.sort(function (a, b) {
      return a[field] > b[field] ? 1 : -1
    })
    if (reverse) filtered.reverse()
    return filtered
  }
})

This filter converts the object into a standard array and sorts it by the field you specify. You can use the orderObjectBy filter exactly like orderBy, including a boolean value after the field name to specify whether the order should be reversed. In other words, false is ascending, true is descending.

<ul>
  <li ng-repeat="item in items | orderObjectBy:'color':true">
    {{ item.color }}
  </li>
</ul>

Note: Because this filter converts the items object to an array, you will no longer have access to the key of the “associative array”, as you do with the (key, item) in items snippet. I did not need the key in my situation, so this was fine.

Update: Thanks to the work of fmquaglia, this code has been turn into an AngularJS module and is available as ngOrderObjectBy for easy integration into your project.

Update: At the request of one of the commenters below, I’m adding license information. The code above is available under the MIT license.

The MIT License (MIT)
Copyright (c) 2013 Justin Klemm

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.