Freeze Sorting in AngularJS

Tables and lists can be sorted very easily and in a very flexible manner in AngularJS. However, lines being edited may move around up and down, when the property that is used for sorting is changed.

Let’s look at an example (excerpt):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<table ng-controller="cityController">
  <thead>
    <tr>
      <th>City</th>
      <th>Temperature</th>
    </tr>
  </thead>
  <tbody ng-repeat="city in cities | orderBy: 'name' ">
    <tr>
      <td>
        <input ng-model="city.name">
      </td>
      <td> °C</td>
    <tr>
  </tbody>
<table>

The corresponding controller only initializes the data:

1
2
3
4
5
6
7
8
9
10
11
12
angular.module('freezeOrder', []).controller('cityController', [
    '$scope',
    function($scope) {
        $scope.cities = [
                         { name: 'Sofia', temperature: 9 },
                         { name: 'Cologne', temperature: 2 },
                         { name: 'Paris', temperature: 3 },
                         { name: 'Madrid', temperature: 12 },
                         { name: 'Lissabon', temperature: 14 }
        ];

]);

Link to complete example

Edit one of the city names. You will notice a problem. Angular updates the model while you are typing and therefore the line moves up and down through the table, depending on where the edited name collates. Disable the sorting would not really help here. What we really want is to freeze the current order.

What can be done here? One option would be to use two overlapping input fields, and swap their z-index while the row is being edited. At the beginning and the end of the editing session, the contents would have to be copied around. Not very elegant, and it would be preferable to have the logic of the solution mostly in the JavaScript code instead of the HTML.

There is such a solution. First we have to extend the HTML a little:

1
2
3
4
5
6
7
8
9
10
<tbody ng-repeat="city in cities | orderBy: order ">
  <tr>
    <td>
      <input ng-model="city.name" 
             ng-focus="freezeOrder()"
             ng-blur="thawOrder()">
    </td>
    <td> °C</td>
  <tr>
</tbody>

The sorting criterion now comes from the scope variable order (line 1), and we also installed handlers for the events focus and blur (lines 5 and 6).

The JavaScript code sees a couple of changes more:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
angular.module('freezeOrder', []).controller('cityController', [
    '$scope',
    '$filter',
    function($scope, $filter) {
        $scope.order = 'name';
        $scope.cities = [
                         { name: 'Sofia', temperature: 9 },
                         { name: 'Köln', temperature: 2 },
                         { name: 'Paris', temperature: 3 },
                         { name: 'Madrid', temperature: 12 },
                         { name: 'Lissabon', temperature: 14 }
        ];
        
        $scope.freezeOrder = function() {
            $scope.cities = $filter('orderBy')($scope.cities, 'name');
            for (var i = 0; i < $scope.cities.length && i <= 9999; ++i) {
                $scope.cities[i]['frozenOrder'] = ("000" + i).slice(-4); 
            }
            $scope.order = 'frozenOrder';
        };
        
        $scope.thawOrder = function() {
            $scope.order = 'name';
        };
    }
]);

We injected a new dependency $filter (lines 3 and 4). We need that in our handler $scope.freezeOrder. In line 15 we sort our original array exactly in the same order in which it is displayed in the browser, that is we sort by the property name.

Then we loop over the array, extending every list element with the new property frozenOrder that contains the position of the list element as a zero-padded 4-digit number (line 17). If you don’t understand the part right of the equals sign, have a look at the documentation of slice. We chose 4-digit zero-padded numbers so that the alphanumeric sorting will produce correct results. In order to keep things simple we hardcoded a limit of 10000 elements for the size of the array (line 16).

Each list item now has a new property frozenOrder that is not displayed in the user interface but can be used for sorting. We therefore write the name of this property in line 19 into the variable containing the sorting criterion.

When the input field loses the focus, the function $scope.thawOrder gets called, and the process gets reverted by writing back “name” in the variable with the sort criterion. Maybe, you want to clean up here and remove the inserted property frozenOrder again.

You can find the working example here.


blog comments powered by Disqus