37

I am ordering a my data and its working all correcty except some fields are empty or have no value. When ordered these empty field come up first. For example when ordering numbers we would get a huge empty list before getting the "0"-values.

I am doing it like thise:

ng-click="predicate = 'name'; reverse=!reverse"

and

ng-repeat="name in names | orderBy:predicate:reverse"

JSFiddle: http://jsfiddle.net/JZuCX/1/

Is there an easy elegant way to fix this? I want the empty fields to come last, no matter what.

deekay
  • 607
  • 2
  • 9
  • 17

9 Answers9

62

How about this for sorting strings:

item in (items|orderBy:['!name', 'name'])

The advantage (apart from being more concise) is it sorts null & undefined with the blank strings.

In my case I wanted the blanks & nulls & undefineds together at the top (nulls and undefineds by default sort to the bottom), so I used:

item in (items|orderBy:['!!name', 'name'])
Sean
  • 14,359
  • 13
  • 74
  • 124
  • this is awesome! How could I do the trick it the attribute is stored in a variable? item in (items|orderBy:['!' + myvarname, myvarname]) Does not work – jorgetutor Mar 26 '16 at 08:39
  • simply awesome, even works with a parameter as order, i.e. `element in elements | orderBy:['!!' + order, order]:reverse` – Guillaume Jun 17 '16 at 02:34
  • @jorgetutor - I think you have your parentheses off. It should be `item in items | orderBy: ['!'+myvarname, myvarname]` – Jeremy Oct 03 '16 at 03:09
  • 1
    This works for numbers as well as strings except for 0 (zero). It groups 0 with `null` and `undefined` values. Is there a way to work with zeros as legit values? – Jeremy Oct 03 '16 at 03:10
  • @jorgetutor Assign the array to the variable first like this: `$scope.order = ['!' + order, order];` Then use it in the orderBy. `item in items | orderBy: order` For some reason it doesn't seem to like the `'!' +` when you put it directly in the pipe. – Syclone Nov 11 '16 at 22:36
  • I can't seem to get this solution to work. Does anybody have a working fiddle? – Coo Dec 26 '16 at 17:19
  • 1
    Here is a version working with strings + number sorting: https://jsfiddle.net/kmturley/cezjga46/ – Kim T Jan 13 '17 at 22:17
  • and programmatically: $filter('orderBy')(filtered, ['!!' + sort, sort], order); – Kim T Jan 13 '17 at 22:24
  • Incredibly clever and concise, I wish I could +100 – Cyril CHAPON Jun 02 '17 at 14:49
  • This doesn't work with reverse. Because on reverse the empty elements are again on top. You need the `-` in front of the second sort `['!' + order, order]:false` and `['!' + order, -order]:true`. – Andi Giga Oct 18 '17 at 09:27
43

I'd write a filter that takes items with empty name from ordered array and places them at the end:

<li ng-repeat="item in (items|orderBy:'name'|emptyToEnd:'name')">{{item.name}}</li>

Code might look like this:

.filter("emptyToEnd", function () {
    return function (array, key) {
        if(!angular.isArray(array)) return;
        var present = array.filter(function (item) {
            return item[key];
        });
        var empty = array.filter(function (item) {
            return !item[key]
        });
        return present.concat(empty);
    };
});

Working example.

By the way, your fiddle doesn't contain any relevant code. Did you use the wrong link?

Update 2: Your fiddle with my filter.

  • Sorry, edited in correct fiddle. How would i integrate your example with my fiddle? – deekay Sep 04 '13 at 03:17
  • @deekay, do `myApp.filter("emptyToEnd...` – sushain97 Sep 04 '13 at 03:31
  • I've added a link to fiddle with integrated filter. – Klaster_1 Нет войне Sep 04 '13 at 03:45
  • 1
    Cool thats it! Why do i get an error TypeError: Cannot call method 'filter' of undefined at Object. in my app? – deekay Sep 04 '13 at 04:01
  • Good point, I forgot to add check for undefined input value. Answer's updated. – Klaster_1 Нет войне Sep 04 '13 at 04:05
  • It breaks the other table filter though :/ but i guess i will figure this one out! – deekay Sep 04 '13 at 04:22
  • If you want the empties to be at the top when reverse sorting, add a `reverse` param to the function, replace the return with `if (reverse) return empty.concat(present); return present.concat(empty);` and add `:reverse` to the filter in your template, where reverse is a flag that your sorts being reversed. – Matt Aug 18 '16 at 10:37
  • 1
    If you want to include zero as a value and not classed as empty swap this bit:var present = array.filter((item) => { return item[key] || item[key] === 0; }); var empty = array.filter((item) => { return !item[key] && item[key] !== 0; }); – MagicLuckyCat Jun 04 '19 at 14:50
  • @Klaster_1Is there any way to prevent this from running when entering data? I have a table that uses ng-repeat and this directive. A table row can be edited by clicking the edit button. When I type a value, it runs the directive and sorts the list automatically. – J.Do Jul 29 '19 at 14:24
  • @J.Do you can update the query value with delay (debounce, throttle, etc) or by some event, like form submit. – Klaster_1 Нет войне Jul 30 '19 at 06:03
16

Down here! :D

This solution extends the normal functionality of the angularJs orderBy filter to take a third argument specifying whether or not to invert the normal sorting of null and undefined values. It observes the property names it is passed (not just one), and doesn't iterate over items a second as some of the other solutions do. It's used like this:

<li ng-repeat="item in (items|orderBy:'name':false:true)">{{item.name}}</li>

I found a bunch of threads, some not directly about orderBy, and compiled their techniques plus a couple bits of my own into this:

angular.module('lib')
.config(['$provide', function ($provide) {
    $provide.decorator('orderByFilter', ['$delegate', '$parse', function ($delegate, $parse) {
        return function () {
            var predicates = arguments[1];
            var invertEmpties = arguments[3];
            if (angular.isDefined(invertEmpties)) {
                if (!angular.isArray(predicates)) {
                    predicates = [predicates];
                }
                var newPredicates = [];
                angular.forEach(predicates, function (predicate) {
                    if (angular.isString(predicate)) {
                        var trimmed = predicate;
                        if (trimmed.charAt(0) == '-') {
                            trimmed = trimmed.slice(1);
                        }
                        var keyFn = $parse(trimmed);
                        newPredicates.push(function (item) {
                            var value = keyFn(item);
                            return (angular.isDefined(value) && value != null) == invertEmpties;
                        })
                    }
                    newPredicates.push(predicate);
                });
                predicates = newPredicates;
            }
            return $delegate(arguments[0], predicates, arguments[2]);
        }
    }])
}]);

To use this code verbatim, be to specify 'lib' as a dependency for your app.

Credits to:

Community
  • 1
  • 1
shannon
  • 8,664
  • 5
  • 44
  • 74
  • 1
    This works like a charm. It's the only solution I've found so far that works correctly with multiple predicates. And it's super clean to boot. – Ted Kulp Aug 24 '15 at 14:38
  • 1
    Unreal! Extending Angular's functionality is a much better approach. – Jamie Counsell Feb 21 '16 at 03:14
  • Since AFAICT smart-tables doesn't allow you to pass additional arguments, I tweaked this to add `var sortDescending = arguments[2]` and then changed the check from invertEmpties to sortDescending; with invertEmpties set to `true`, this decorator keeps null/empty at the bottom regardless of sort direction when using smart-tables. – Dave DuPlantis Jun 28 '17 at 22:01
  • This only checks for null values... but can easily be modified to also put empty strings and undefined values last as well by modifying: `(angular.isDefined(value) && value != null)` – nawlbergs Jul 02 '17 at 12:38
4

I don't believe there's an "out of the box" solution for this. I could easily be wrong. Here's my attempt at a solution using a function as the predicate:

ng-repeat="name in names | orderBy:predicate"

Inside your controller:

$scope.predicate = function(name) {
    return name === '' ? 'zzzzzzz' : !name; 
    /* The 'zzzzzz' forces the empty names to the end, 
      I can't think of a simpler way at the moment. */
}
sushain97
  • 2,752
  • 1
  • 25
  • 36
4

In addition to the solution of Klaster_1, add an extra parameter to make the filter more generic:

http://jsfiddle.net/Zukzuk/JZuCX/27/

Implementation

<tr ng-repeat="name in (names | orderBy:predicate:reverse | orderEmpty:'name':'toBottom')">

Filter

.filter('orderEmpty', function () {
    return function (array, key, type) {
        var present, empty, result;

        if(!angular.isArray(array)) return;

        present = array.filter(function (item) {
            return item[key];
        });

        empty = array.filter(function (item) {
            return !item[key]
        });

        switch(type) {
            case 'toBottom':
                result = present.concat(empty);
                break;
            case 'toTop':
                result = empty.concat(present);
                break;

                // ... etc, etc ...

            default:
                result = array;
                break;
        }
        return result;
    };
});

Thnx Klaster_1!

Zukzuk
  • 73
  • 6
2

Sorting, and reverse sorting, using a variable sort column, and keeping the undefined at the bottom, even below the negative values

I love the elegance of Sean's answer above! I needed to give my users the ability to choose the column to sort on, and choice of sort direction, but still require the undefined's to fall to the bottom, even if there are negative numbers.

The key insight from Sean that fixes negative numbers is !!. Use '!'+predicate if you are doing forward sorting and '!!'+predicate if you are doing reverse sorting.

The snippet below demonstrates this. By the way, I have put the variables that set the predicate (choice of propery to sort on) and reverse inside an object ("d") just so that we don't get weird scope issues. You may not need the "d."s in your environment.

Moreover you would probably want to use something better than my crappy buttons at the bottom of the page to control your sort predicate and direction. However this keeps the key parts of the code easy to read.

function mainController($scope) {
  $scope.userArray = [
  { name: "Don", age: 20 },
  { name: "Bob", age: 30, height: 170 },
  { name: "Abe", age: 40, height: 160 },
  { name: "Zoe", age: 70 },
  {              age: 70, height: 155 },
  { name: "Shorty",age:45,height: -200},
  { name: "TwinkleInEye", age: -1, height: 152 }
  ]

  $scope.d = {}; // Create an object into which info can be stored and not trashed by Angular's tendency to add scopes
  $scope.d.predicate = "name"; // This string is the name of the property on which to sort
  $scope.d.reverse = false; // True means reverse the sort order
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<body ng-app="" ng-controller="mainController">
  <div ng-repeat="user in (userArray | orderBy: (d.reverse ?['!!'+d.predicate,d.predicate]:['!'+d.predicate,d.predicate]) : d.reverse)">

    Name {{ user.name }} : Age {{ user.age }} : Height {{ user.height }}
  </div>

  <br/>

  <button ng-click="d.predicate='name';">Name</button>
  <button ng-click="d.predicate='age';">Age</button>
  <button ng-click="d.predicate='height';">Height</button> Currently: {{d.predicate}}
 <br/> Leave undefined at bottom, but otherwise: 
  <button ng-click="d.reverse= !d.reverse;">Reverse</button> Currently: {{d.reverse}}

</body>
ProfDFrancis
  • 8,816
  • 1
  • 17
  • 26
0

@Klaster_1 was really on to something but as soon as I needed a nested value the filter stopped working. Also, if I was reverse ordering I still wanted my null values to show up before 0. I added $parse to take care of the nested keys and added a reverse parameter to I knew when to put the null values at the top.

.filter("emptyToEnd", function ($parse) {
    return function (array, key, reverse) {
        if(!angular.isArray(array)) return;
        var keyFn = $parse(key);
        var present = [];
        var empty = [];

        angular.forEach(array, function(item){
          var val = keyFn(item);
          if(angular.isUndefined(val) || val === null) {
            empty.push(item);
          } else {
            present.push(item);
          }
        });

        if (reverse) {
          return present.concat(empty);
        } else {
          return empty.concat(present);
        }
    };
});
kt miller
  • 13
  • 4
  • 1
    This to note, this doesn't work with negative keys. Meaning that when you are using the "-keyToSortOn" this filter falls on its face. – kt miller Jul 10 '14 at 22:06
0

I don't know why other answer suggest to put the null value records at the bottom, If I want to sort normally, means in ASC order all the null on top and in DESC order all the nulls go to bottom, I tried other answers here but could not helped me so change the code to convert the null to '' in my array and it works now smooth like this:

$scope.convertNullToBlank = function (array) {
  for (var i = 0; i < array.length; i++) {
     if (array[i].col1 === null)
       array[i].col1 = '';

     if (array[i].col2 === null)
        array[i].col2 = '';
  }
  return array;
}
Ali Adravi
  • 21,707
  • 9
  • 87
  • 85
0

I created a gist with an alternative filter based on the previous solutions: https://gist.github.com/360disrupt/1432ee1cd1685a0baf8967dc70ae14b1

The filter extends the existing angular filter:

angular.module 'tsd.orderByEmptyLast', []
  .filter 'orderByEmptyLast', ($filter) ->
    return (list, predicate, reverse)->
      orderedList = $filter('orderBy')(list, if reverse then ['!' + predicate, '-' + predicate] else ['!' + predicate, predicate] )
      return orderedList

On newer angular versions you might need to include orderByFilter instead of using $filter

angular.module 'tsd.orderByEmptyLast', ['orderByFilter']
  .filter 'orderByEmptyLast', () ->
    return (list, predicate, reverse)->
      orderedList = orderByFilter(list, if reverse then ['!' + predicate, '-' + predicate] else ['!' + predicate, predicate] )
      return orderedList
Andi Giga
  • 3,744
  • 9
  • 38
  • 68