Sortierung in AngularJS einfrieren

Tabellen und Listen lassen sich mit AngularJS sehr leicht und auf flexible Art sortieren. Bei CRUD-Applikationen bewegt sich die Zeile beim Editieren aber durch die Gegend, wenn ausgerechnet das Sortierkriterium geändert wird.

Schauen wir uns ein Beispiel (gekürzt) an:

<table ng-controller="cityController">
  <thead>
    <tr>
      <th>Stadt</th>
      <th>Temperatur</th>
    </tr>
  </thead>
  <tbody ng-repeat="city in cities | orderBy: 'name'">
    <tr>
      <td>
        <input ng-model="city.name">
      </td>
      <td>{{city.temperature}} °C</td>
    <tr>
  </tbody>
<table>

Der zugehörige Controller initialisiert lediglich die Daten:

angular.module('freezeOrder', []).controller('cityController', [
    '$scope',
    function($scope) {
        $scope.cities = [
                         { name: 'Sofia', temperature: 9 },
                         { name: 'Köln', temperature: 2 },
                         { name: 'Paris', temperature: 3 },
                         { name: 'Madrid', temperature: 12 },
                         { name: 'Lissabon', temperature: 14 }
        ];

]);

Link zum vollständigen Beispiel

Jetzt editieren wir einen der Städtenamen, und haben ein Problem. Angular aktualisiert das Model während des Tippens, und dadurch wandert die Zeile durch die Tabelle. Die Sortierung abzuschalten würde nicht wirklich helfen. Was wir wirklich wollen, ist die Sortierung einzufrieren.

Was tun? Eine Möglichkeit wäre, zwei überlappende Inputfelder darzustellen, deren z-Index sich während des Editierens ändert. Am Anfang und Ende müsste der Inhalt dann durch die Gegend kopiert werden. Nicht sehr elegant, und es wäre außerdem auch schöner, wenn die Lösung größtenteils im JavaScript-Code versteckt wäre.

Es gibt einen solchen Weg. Dazu müssen wir das HTML geringfügig erweitern:

<tbody ng-repeat="city in cities | orderBy: order ">
  <tr>
    <td>
      <input ng-model="city.name" 
             ng-focus="freezeOrder()"
             ng-blur="thawOrder()">
    </td>
    <td>{{city.temperature}} °C</td>
  <tr>
</tbody>

Neu ist, dass das Sortierkriterium aus der Scope-Variablen order kommt (Zeile 1), und außerdem installieren wir Handler für die Events focus und blur (Zeilen 5 und 6).

Im JavaScript passiert natürlich etwas mehr:

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';
        };
    }
]);

Zunächst einmal haben wir eine neue Abhängigkeit $filter injizieren müssen (Zeilen 3 und 4). Die brauchen wir in unserem Handler $scope.freezeOrder. Wir sortieren unser Originalarray in Zeile 15 exakt so, wie es auch im Moment im Browser sortiert dargestellt ist, sortieren nämlich nach dem Inhalt des Properties name.

In der darauffolgenden Schleife iterieren wir über das Array und fügen zu jedem Element eine Eigenschaft frozenOrder zu, in die wir die Position des Listenelements als vierstellige Zahl schreiben (Zeile 17). Wer den Teil rechts des Gleichheitszeichens nicht versteht, sei auf die Dokumentation von slice verwiesen. Die Zahlen sind deshalb vierstellig und alle mit Nullen aufgefüllt, damit die alphanummerische Sortierung korrekte Ergebnisse liefert. Weiterhin haben wir der Einfachheit halber ein hartes Limit von 10000 Elementen für die Größe des Arrays eingebaut (Zeile 16).

Jedes Listenelement hat jetzt eine neue Eigenschaft frozenOrder, die in der Oberfläche nicht dargestellt wird, nach der aber sortiert werden kann. Deshalb schreiben wir den Namen der Eigenschaft in Zeile 19 in die Variable mit dem Sortierkriterium.

Verliert das Inputfeld den Focus wird die Funktion $scope.thawOrder aufgerufen, die das Ganze rückgängig macht, indem wieder name als Sortierkriterium verwendet wird. Hier könnte man natürlich noch aufräumen und das neu eingefügte Feld frozenOrder wieder entfernen.

Das vollständige funktionierende Beispiel findet sich hier.


blog comments powered by Disqus