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

AngularJS

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.

  • http://www.calendee.com Justin Noel

    Thanks for this. I ran into the same problem. Coming from a PHP background, JavaScripts differences with “associate arrays” have been killing me. With my AngularJS app, I gave up with the associative array ordering properly.

    Now you’ve shown I should have dug a bit deeper.

  • Jacob Evan Shreve

    Thanks! I was getting very frustrated with orderBy not working on my object of objects, and not getting any error feedback.

  • Adam Brooks

    Much appreciated Justin!

  • park seung hyun

    http://digveloper.ppillip.com/?p=389

    i add that “sort by key”

    • Thomas Waldecker

      Thanks for sharing!

  • Michael Taranto

    This was a great solution. My only issue I had was that it worked in Chrome but failed in my unit tests which were being run in PhantomJS. I narrowed it down to the compare function that gets passed to sort returning a boolean instead of 1, 0, or -1.

    So rather than
    filtered.sort(function (a, b) {
    return (a[field] > b[field]);
    });

    I changed it to
    filtered.sort(function (a, b) {
    return (a[field] > b[field]) ? 1 : ((a[field] < b[field]) ? -1 : 0);
    });

    Now it works everywhere and passes in my unit tests too

    • http://justinklemm.com/ Justin Klemm

      Michael, thanks for the heads up on this! I’ll do some testing and look at changing the code in the post.

  • Nir Melamoud

    Thanks, you are the best!

  • thallisphp

    Thanks, you saved my night =D

  • http://www.k15t.com/ Stefan Kleineikenscheidt

    Thanks Justin. Any idea, how this could make it into AngularJS?

  • Logan

    Exactly what I was looking for while trying to sort a collection. I needed the key, but fortunately, I had this available as an attribute in the collection, so I just used {{item[0].key}} in my view. Works great! Thank you!

    • http://justinklemm.com/ Justin Klemm

      Logan, glad it was useful! Note that if you end up needing the key in a collection where it’s not available, a developer commented further down with a solution that adds it to each item: http://digveloper.ppillip.com/?p=389

      • Logan

        Good to know!

      • Alexander Nyquist

        As I see it, the more angular-way would be to name it $key instead of just key.

  • Bartlomiej Skwira

    Here is a CoffeeScript version I used

    yourApp.filter “orderObjectBy”, ->

    (items, field, reverse) ->

    filtered = []

    angular.forEach items, (item) ->

    filtered.push item

    filtered.sort (a, b) ->

    a[field] > b[field]

    filtered.reverse() if reverse

    filtered

    later also user the ‘key’ version modified to id (thats the key in my collection)

    myApp.filter “orderObjectBy”, ->

    (items, field, reverse) ->

    filtered = []

    angular.forEach items, (item, key) ->

    item['id'] = key

    filtered.push item

    filtered.sort (a, b) ->

    a[field] – b[field]

    filtered.reverse() if reverse

    filtered

  • Plasmed

    Thanks a lot!

  • http://www.woodyhayday.com Woody Hayday

    This is Killer.

    Useful :)

    Thanks Justin!

  • Pachito Marco Calabrese

    Super!

  • Claus Colloseus

    Another, even shorter solution would be to convert the object into an array before you start to repeat:
    ng-repeat=”item in itemsToArray(items) | orderBy: ‘color'”

    and

    $scope.itemsToArray = function (items) {
    var array = [];
    angular.forEach($scope.words, function(item) {
    array.push(item);
    });
    return array;

    };

  • http://magic.indivly.com/ John Clark

    This code was sorting last/first element wrong for some reason. Used toArray filter. Usage: ng-repeat=”x in y | toArray | orderBy:’color':true”
    app.filter(‘toArray’, function () {
    ‘use strict';

    return function (obj) {
    if (!(obj instanceof Object)) {
    return obj;
    }

    return Object.keys(obj).map(function (key) {
    return Object.defineProperty(obj[key], ‘$key’, {__proto__: null, value: key});
    });
    }
    });
    source: https://github.com/angular/angular.js/issues/1286

  • George Bora

    Thanks, I was stuck on why orderBy didn’t work and you provided both a explanation and a solution.

  • http://www.kettenba.ch $(‘#c.kettenbach’)

    How can I set a default value and not get the empty option? Like when I’m using a hash of hashes object data source. Thanks, this post helped me a lot.

    • http://justinklemm.com/ Justin Klemm

      I’m not totally sure I know what you mean… but if you’re asking about showing some other element if the list if empty, you could try adding something like this below the

      No entries

  • jorenm

    Here’s a version I wrote for coffeescript. A bit modified so it accepts an array of keys, so you can filter to an arbitrary point in a nested object.

    https://gist.github.com/Jorenm/6d1a7be9d4d7ce1d9208

  • Janne

    This was a great post and a good solution, thank you for this! The only problem for me was that I need the indexes of the original object to be used in the template. Like: { “JH937NSK”: { data: 0 }, “VNDK9977S”: { data: 1 } }.

    So I changed it a bit, to return object that holds both:

    return function(items, field, reverse) {
    var helper = [];
    angular.forEach(items, function(item, index) {
    helper[item[field]] = { “item”: item, “index”: index };
    });

    if(reverse) helper.reverse();

    return helper;
    };

    Which can be used in the template via obj.item and obj.index references.

    This is just kinda tacky / messy I think, because it changes the object being ordered. I didn’t find a better solution, since objects can not be ordered reliably (I even tried to create a new object in different order, but it didn’t work in chrome at least and always ordered it alphabetically). If someone has a better solution, please bring that on the table too :).

    • jorenm

      You definitely don’t want to change the object being ordered in an orderBy filter. I think perhaps what you want is this: http://stackoverflow.com/questions/15127834/how-can-i-iterate-over-the-keys-value-in-ng-repeat-in-angular

      • Janne

        Well the thing is that, even the original script already changes the object being ordered :). Since it transforms the object to an array, it will not receive the original indexes:

        Having a data like this: datas = { “JH937NSK”: { data: 0 }, “VNDK9977S”: { data: 1 } }

        after being processed through: ng-repeat=”data in datas | orderObjectBy:’location'”

        Filter would produce: [ { data: 0 }, { data: 1 } ] . So in this case the indexes are: 0 and 1, not “JH937NSK” and “VNDK9977S” as they should be.

        So I need the original index, which gets lost after orderObjectBy-filter has processed the data.

        The filter that I posted, seems to cause an infDig-error.

        • jorenm

          I see what your issue is now. The infDig error is the result of changing the original object in the filter. The object you’re ng-repeating on is watched by angular, and when it changes, it runs the filter. The problem arises because you’re triggering the change listener inside the filter, causing infinite recursion.

          My code doesn’t change the original object, it simply returns an ordered array version of that object. Here’s a new gist that I think has what you want. I didn’t test it, but you should get the idea. It preserves the key you wanted, without modifying the original object. https://gist.github.com/anonymous/3667d715788465b63874

          • Janne

            Thank you for the help! I actually still have the problem, that if I change the original object at all, like you wrote: filtered.push({key: key, value: item}), it will go to a loop.

            Eventhough the actual problem is not solved, I can still work around the problem and insert a .id-property to the original objects (which corresponds to the key / index) – the bad side is that I have to keep the index and the .id-property in sync for the program to work correctly.

  • franci

    If you need the keys, add <>

    BEFORE:
    angular.forEach(items, function(item) {
    filtered.push(item);
    });

    AFTER:
    angular.forEach(items, function(item) {
    item["key"]=key;
    filtered.push(item);
    });

    • franci

      I forgot this:
      angular.forEach(items, function(item, key) {

  • http://www.adostudio.it Andrea Dell’Orco

    Thanks for your filter! Works very good

  • Ariel Fuggini

    Thanks! Great tip

  • http://outloudthinking.me/ Fabricio Matías Quagliariello

    Nice, useful and simple. You saved my day. Thanks!

  • J. Kurian

    Thanks for the example, but, my array contains object with ids that I’m trying to sort by a status string and the problem is the ids are getting switched around for some reason.

    Working on a Gist to share…

  • leticia

    Thanks!

  • http://nadim.computer/ Nadim Kobeissi

    Thanks so much for this.

  • http://blog.a2merch.ca/ François Aucoin

    Great article. Thank you for sharing. :)