Angularjs table sort with ng-repeat
Asked Answered
B

3

9

I have an HTML table and want to sort my records ($scope.records in ctrl) by clicking on table headers ($scope.headers in ctrl),

Can anyone explain why does that work:

<th>
    <a ng-click="sortColumn=headers[0];reverse=!reverse">{{ headers[0] }}</a>
</th>
<th>
    <a ng-click="sortColumn=headers[1];reverse=!reverse">{{ headers[1] }}</a>
</th>

And that doesn't:

<th ng-repeat="header in headers">
    <a ng-click="sortColumn=headers[$index];reverse=!reverse">{{ headers[$index] }}</a>
</th>

Here is the code for the records:

<tr ng-repeat="arr in records | orderBy:sortColumn:reverse">
    <td ng-repeat="val in arr" ng-bind-html-unsafe="arr[headers[$index]]</td>
</tr>

I have 58 columns in my table so would be much better to loop through the table headers...

Bibliography answered 5/10, 2012 at 10:57 Comment(2)
What, exactly, does not work about the second solution? Off-hand, I'm going to guess that in your <td ng-repeat> $index is being bound to the td repeat, not the tr repeat, but I don't quite have enough information to say for certain. Have you tried looking at the scope using batarang?Geophilous
PLease see Gloopy-s reply, I think I learned something new about primitives in the repeat scope ;) thxBibliography
T
12

As David suggested this is likely scope related. Since ngRepeat creates a new scope your ngClick is setting the sortColumn and reverse in its own child scope for each column header.

One way around this to ensure you are modifying the values in the same scope would be to create a function on the scope and call that in your ngClick passing in the index:

$scope.toggleSort = function(index) {
    if($scope.sortColumn === $scope.headers[index]){
        $scope.reverse = !$scope.reverse;
    }                    
    $scope.sortColumn = $scope.headers[index];
}

with this as your markup:

<th ng-repeat="header in headers">
    <a ng-click="toggleSort($index)">{{ headers[$index] }}</a>
</th>

Here is a fiddle with an example.


Another option would be to bind to a non-primitive type like this (the child scopes will be accessing the same object):

$scope.columnSort = { sortColumn: 'col1', reverse: false };

with this as your markup:

<th ng-repeat="header in headers">
    <a ng-click="columnSort.sortColumn=headers[$index];columnSort.reverse=!columnSort.reverse">{{ headers[$index] }}</a>
</th>

Here is a fiddle with an example.

Toxic answered 5/10, 2012 at 15:21 Comment(1)
Thanks a lot, it's working! Really appreciate your help, I found the second solution to be ... hmmm ... just nicer as I don't need to create a new sort function in my controller. Thanks again!!Bibliography
S
3

Extending Gloopy's answer, yet another option is to modify the parent's properties in the ng-repeat for the primitive types:

<a ng-click="$parent.sortColumn=headers[$index];$parent.reverse=!$parent.reverse">{{ headers[$index] }}

Here is a fiddle.

Note however that $parent is not a documented property of scope, so this is somewhat of a hack, so use at your own risk.

I wish AngularJS had a better way of dealing with these "inner scopes" that are created by ng-repeat, ng-switch, etc. because quite often we need to modify parent scope properties that are primitives.

See also Gloopy's insightful comment about scope inheritance as it relates to primitives and non-primitives here.

Snappy answered 5/10, 2012 at 16:2 Comment(0)
D
0

I don't know what sort of data is in your records, so for my sample I just used an array of JSON values. I have tried several different sorting plugins for my Javascript with Angular and nothing worked. In the long run I have discovered you don't necessarily need those extras.

Since AngularJS is good at processing javascript data-structures for displaying in HTML, you can just rearrange the javascript-arrays in memory, and AngularJS picks up on the changes. This example allows clicking the headers of the table, which will trigger a sorting based on that columns data type. If it is already sorted on that column, it will reverse-sort the column. The type detection is done through the presented isNumeric() function, and one two-tiny tweaks:

  1. Added checks if inputting on the '#' symbol as a header and sorts as a number in the toggleSort method. This can easily be removed by users if preferred.
  2. When toggleSort attempts to sort alphabetically, if it catches a TypeError it then switches to sorting on numbers.

var app = angular.module("app", []);

app.controller("MainController", function($scope) {

  $scope.samplePositions = [
  	{"#": "1", "Unique ID": "100130", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 1", "Status": "Available"},
  	{"#": "2", "Unique ID": "100131", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 2", "Status": "Available"},
  	{"#": "3", "Unique ID": "100132", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 3", "Status": "Available"},
  	{"#": "4", "Unique ID": "100133", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 4", "Status": "Available"},
  	{"#": "5", "Unique ID": "100134", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 5", "Status": "Checked Out"},
  	{"#": "6", "Unique ID": "100135", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 6", "Status": "Checked Out"},
  	{"#": "7", "Unique ID": "100136", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 7", "Status": "Checked Out"},
  	{"#": "8", "Unique ID": "100137", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 8", "Status": "Checked Out"},
  	{"#": "9", "Unique ID": "100138", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 1 - Box 1 - Position 1", "Status": "Available"},
  	{"#": "10", "Unique ID": "100139", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 2 - Box 1 - Position 1", "Status": "Available"},
  	{"#": "11", "Unique ID": "100140", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 2 - Box 1 - Position 2", "Status": "Available"},
  	{"#": "12", "Unique ID": "100141", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 2 - Box 1 - Position 3", "Status": "Lost"},
  	{"#": "13", "Unique ID": "100142", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 2 - Box 1 - Position 4", "Status": "Lost"},
  	{"#": "14", "Unique ID": "100143", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 3 - Box 1 - Position 1", "Status": "Available"},
  	{"#": "15", "Unique ID": "100144", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 3 - Box 1 - Position 2", "Status": "Available"},
  	{"#": "16", "Unique ID": "100145", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 4 - Box 1 - Position 1", "Status": "Checked Out"},
  	{"#": "17", "Unique ID": "100146", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 4 - Box 1 - Position 2", "Status": "Available"},
  	{"#": "18", "Unique ID": "100147", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 4 - Box 1 - Position 3", "Status": "Available"},
  	{"#": "19", "Unique ID": "100148", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 4 - Box 1 - Position 4", "Status": "Checked Out"},
  	{"#": "20", "Unique ID": "100149", "Name": "Book", "Section": "Paraguay", "Position": "Shelf 1 - Rack 5 - Box 1 - Position 1", "Status": "Available"}		
  ]
  
  // Dynamically get the entry headers to use with displaying the nested data via header-key lookups
  // Assumes all lines contain same key-text data
	$scope.samplePositionsHeaderKeys = []; // Contains only the key-data, not the values
	for (var key in $scope.samplePositions[0]) {
		if ($scope.samplePositions[0].hasOwnProperty(key)) {
			$scope.samplePositionsHeaderKeys.push(key);
		}
	}
  
		/**
		 * Determine if the input value is a number or not.
	   * @param n The input value to be checked for numeric status.
		 * @returns true if parameter is numeric, or false otherwise.
		 * 
		 * This method uses the following evaluations to determine if input is a numeric:
		 * 
		 * 		(5); // true  
		 * 		('123'); // true  
		 * 		('123abc'); // false  
		 * 		('q345'); // false
		 * 		(null); // false
		 * 		(""); // false
		 *		([]); // false
		 * 		('   '); // false
		 * 		(true); // false
		 * 		(false); // false
		 * 		(undefined); // false
		 * 		(new String('')); // false
		 * 
		 * @author C.D. (modified by)
		 * @original https://mcmap.net/q/73284/-check-whether-variable-is-number-or-string-in-javascript
		 * 
		 */
		function isNumeric(n) {
			if (!isNaN(parseFloat(n)) && !isNaN(n - 0) && n !== null && n !== "") {
				return true;
			}
			return false;
		}

		/**
		 * Column Sort Method (generic). Sort based on target column header or reverse sort if already selected on that.
		 * @param dataSource The array of JSON data to be sorted
		 * @param headers The array of JSON object-keys (table column headers) to be referenced
		 * @param index The target JSON object-key to sort the table columns based upon
		 * 
		 * @author C.D.
		 */
		$scope.lastSortIndex = 0;
		$scope.toggleSort = function (dataSource, headers, index) {
			if ($scope.lastSortIndex === index) {
				dataSource.reverse();
			}
			else {
				var key = headers[index];
				if (key === "#" || isNumeric(dataSource[key])) { // Compare as numeric or on '#' sign
					dataSource.sort((a, b) => parseFloat(a[key]) - parseFloat(b[key]));
				}
				else // Compare as Strings
				{
					try { // Attempt to sort as Strings
						dataSource.sort((a, b) => a[key].localeCompare(b[key]));
					} catch (error) {
						if (error.name === 'TypeError') { // Catch type error, actually sort as Numeric
							dataSource.sort((a, b) => parseFloat(a[key]) - parseFloat(b[key]));
						}
					}
				}
				$scope.lastSortIndex = index;
			}
		}

});
<html ng-app="app">

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
  <title>AngularJS - Hello World</title>
  <script data-require="jquery@*" data-semver="3.1.1" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
  <script data-require="[email protected]" data-semver="1.3.13" src="https://code.angularjs.org/1.3.13/angular.js"></script>
  <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
  <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">

  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-controller="MainController">
  <div class="container">
    <table class="table table-hover	table-sm">
  		<thead>
  			<tr>
  				<th ng-repeat="header in samplePositionsHeaderKeys">
  					<a ng-click="toggleSort(samplePositions, samplePositionsHeaderKeys, $index)">{{ header }}</a>
  				</th>
  			</tr>
  		</thead>
  
  		<tbody>
  			<!-- Data is nested, so double-repeat to extract and display -->
  			<tr ng-repeat="row in samplePositions" >
  				<td ng-repeat="key in samplePositionsHeaderKeys">
  					{{row[key]}}
  				</td>
  			</tr>
  		</tbody>
  	</table>
  </div>
</body>

</html>

I have put together a working Plunker example to demonstrate. Just click on the headers and they will sort the array in memory, where AngularJS will pick up on the changes and refresh that portion of the DOM.

Diphenyl answered 4/10, 2019 at 1:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.