125

I'm after a table sorting solution (in JavaScript) but I can't seem to find a suitable one yet. I just need it to sort each column alphabetically. It doesn't need to ignore any code or any numbers or to work with currency. Just a click on the column header switches it from sorted a-z/z-a.

Does anyone know of a really simple solution like this?

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
lukeseager
  • 2,365
  • 11
  • 37
  • 57

18 Answers18

264

Just revisiting an old solution, I thought I'd give it a facelift for it's ~5 year anniversary!

  • Plain Javascript (ES6)
  • Does alpha and numeric sorting - ascending and descending
  • Works in Chrome, Firefox, Safari (and IE11, see below)

Quick explanation

  1. add a click event to all header (th) cells...
  2. for the current table, find all rows (except the first)...
  3. sort the rows, based on the value of the clicked column...
  4. insert the rows back into the table, in the new order.

const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;

const comparer = (idx, asc) => (a, b) => ((v1, v2) => 
    v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
    )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

// do the work...
document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
    const table = th.closest('table');
    Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
})));
table, th, td {
    border: 1px solid black;
}
th {
    cursor: pointer;
}
<table>
    <tr><th>Country</th><th>Date</th><th>Size</th></tr>
    <tr><td>France</td><td>2001-01-01</td><td><i>25</i></td></tr>
    <tr><td><a href=#>spain</a></td><td><i>2005-05-05</i></td><td></td></tr>
    <tr><td><b>Lebanon</b></td><td><a href=#>2002-02-02</a></td><td><b>-17</b></td></tr>
    <tr><td><i>Argentina</i></td><td>2005-04-04</td><td><a href=#>100</a></td></tr>
    <tr><td>USA</td><td></td><td>-6</td></tr>
</table>

IE11 Support (non-ES6)

If you want to support IE11, you'll need to ditch the ES6 syntax and use alternatives to Array.from and Element.closest.

i.e.

var getCellValue = function(tr, idx){ return tr.children[idx].innerText || tr.children[idx].textContent; }

var comparer = function(idx, asc) { return function(a, b) { return function(v1, v2) {
        return v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2);
    }(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
}};

// do the work...
Array.prototype.slice.call(document.querySelectorAll('th')).forEach(function(th) { th.addEventListener('click', function() {
        var table = th.parentNode
        while(table.tagName.toUpperCase() != 'TABLE') table = table.parentNode;
        Array.prototype.slice.call(table.querySelectorAll('tr:nth-child(n+2)'))
            .sort(comparer(Array.prototype.slice.call(th.parentNode.children).indexOf(th), this.asc = !this.asc))
            .forEach(function(tr) { table.appendChild(tr) });
    })
});

Comparer function breakdown

For the sake of brevity, I compacted the comparer() function. It's a little complex/hard to read, so here it is again exploded/formatted/commented.

// Returns a function responsible for sorting a specific column index 
// (idx = columnIndex, asc = ascending order?).
var comparer = function(idx, asc) { 

    // This is used by the array.sort() function...
    return function(a, b) { 

        // This is a transient function, that is called straight away. 
        // It allows passing in different order of args, based on 
        // the ascending/descending order.
        return function(v1, v2) {

            // sort based on a numeric or localeCompare, based on type...
            return (v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)) 
                ? v1 - v2 
                : v1.toString().localeCompare(v2);
        }(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
    }
};
Ian Y.
  • 2,293
  • 6
  • 39
  • 55
Nick Grealy
  • 24,216
  • 9
  • 104
  • 119
  • 11
    Note that the statement th.parentNode.parentNode fails if the header row is wrapped in a element. I'd suggest using "var table = th.closest('table');". Note that Element.closest() doesn't seem to be implemented in IE11/Edge. – Ed Griebel Mar 09 '18 at 17:21
  • 20
    A comment for newbies in javascript like me, because I've lost a lot of time to understand why it doesn't work for me. So, if you just put this script in your html in – Vladimir May 09 '18 at 12:47
  • 2
    @NickGrealy Thanks for graceful code! What is this designation called: `this.asc = !this.asc` – krolovolk Jul 11 '18 at 07:25
  • @dsb ... not sure... "variable inversion"? :) – Nick Grealy Jul 11 '18 at 23:22
  • 1
    @NickGrealy cool method to toggle a boolean! Just want to reach your high level of programming – krolovolk Jul 12 '18 at 12:36
  • 2
    Is there a solution that handles prefixes like $ or other currencies? – Kyle Underhill Aug 07 '18 at 21:42
  • 1
    @KyleUnderhill - simply strip all the currency characters (`$` ,etc) before doing the compare -> and the currencies will be treated as numbers. As for the `,` delimiter, it means different things in different locales (i.e. "thousand" separator (Australia) vs "decimal" separator (Europe)). I'll leave that solution to you. – Nick Grealy Aug 07 '18 at 23:45
  • 1
    Excellent! By the way, `Array.from(th.parentNode.children).indexOf(th)` might be written `this.cellIndex` (`cellIndex` is well supported by browsers [https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement#Browser_compatibility] ) – Pierre Nov 14 '18 at 22:47
  • 1
    @KyleUnderhill - I had to work with file sizes KB, MB, GB, etc., so I used `myTD.textContent` to display the formatted sizes and internally `myTD.dataset.v` in order to sort the sizes - this means also to update `getCellValue` by inserting `=> tr.children[idx].dataset.v ||` at the very beginning of the code. – Pierre Nov 14 '18 at 23:06
  • 4
    If your rows are inside a `` tag and this answer acts up, see [this tweak](https://stackoverflow.com/a/53880407/736937) to this answer. – jedwards Dec 21 '18 at 06:54
  • Hey all. I used that code (code for IE) to sort my table and it worked fine except if we have numbers in the string of the value. For example "hallo 25.Months" and "hallo 7.Months". The sorter will sort the 25 before the 7. Even if I tell the comparer that he should be {numeric: true}. Someone has an idea? – albert Mar 07 '19 at 15:19
  • @albert - see my response to KyleUnderhill (re: stripping non-numeric characters). – Nick Grealy Mar 07 '19 at 22:39
  • Remove comment, add code inside `javascript:(function(){ ... })();` and you get nice bookmarklet – ViliusL Jul 04 '19 at 07:55
  • any ideas how to make this work so that a final summary row doesn't get sorted? – Orcra Sep 04 '19 at 13:03
  • This worked great for dates when I was using the `2020-03-17` date format, but now my client wants the dates to appear like `17-Mar-20`. Date sorter no longer works. Is there a solution for this? – kbdev Mar 17 '20 at 14:25
  • 1
    @kbdev - there is a solution! - add your own custom date detection, parsing and sorting logic to the `comparer` function (very easy to do). – Nick Grealy Mar 17 '20 at 21:47
  • I'm getting the error: "Uncaught TypeError: Cannot read property 'asc' of undefined at HTMLTableCellElement.", any idea why? – getup8 May 27 '20 at 15:53
  • Ah, it was because I was jusing – getup8 May 27 '20 at 16:01
  • Could the same functionality also be applied to when you click a button that exists within the Table Header row? For example, fi there is a glyphicon which you would prefer to click rather than the general table header row. – Stephen85 Aug 27 '20 at 06:22
  • @Stephen85 - Absolutely you could do that. Wouldn't take much extra code to support it. – Nick Grealy Aug 30 '20 at 12:46
  • The `comparer` function is _extremely_ difficult to read. I have spent hours trying to make sense of it. Can you please explain what the hell is going on there? It's giving me a headache. This is a request from someone with years in JavaScript development. I understand that we're dealing with closures here, but that doesn't help much. – Pyromonk May 12 '21 at 14:21
  • 1
    @Pyromonk yes, could be better explained. I've updated the solution. If you still need help, please reach out. – Nick Grealy May 12 '21 at 23:46
  • @NickGrealy, thank you, I had figured it out eventually (after 6 hours or so), and your expanded answer definitely helped! I'm still wondering how one actually writes a function like that... I don't think I could use such a trick for passing parameters even after understanding it. It seems kind of counterintuitive (but very helpful in certain scenarios at the same time). – Pyromonk May 24 '21 at 12:03
  • 1
    Thank you! I have my table in a popup, which can be opened/closed multiple times, so had to change ```const``` for ```var```, as cannot redeclare ```const's``` – michal Jun 03 '21 at 14:09
  • 1
    Trouble sorting numbers with affixes (e.g. `$1`, `0.5%`)? Instead of stripping them on the fly, add the raw value as a cell data-attribute, e.g. ```data-value="0.5"```, and then update the ```getCellValue``` function with ```tr.children[idx].dataset['value'] || tr.children[idx].innerText``` to look for the raw data before using innerText. Should perform better on larger datasets. – Markus AO Jan 23 '22 at 06:42
  • 1
    Very nice! I had to check https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild to get why table.appendChild(tr) does not double the current tr. MDN states: " If the given child is a reference to an existing node in the document, appendChild() moves it from its current position to the new position". – Donapieppo May 24 '22 at 15:07
  • This is working almost, I have to double click for it to work... – Joe Shakely Oct 27 '22 at 18:50
86

I wrote up some code that will sort a table by a row, assuming only one <tbody> and cells don't have a colspan.

function sortTable(table, col, reverse) {
    var tb = table.tBodies[0], // use `<tbody>` to ignore `<thead>` and `<tfoot>` rows
        tr = Array.prototype.slice.call(tb.rows, 0), // put rows into array
        i;
    reverse = -((+reverse) || -1);
    tr = tr.sort(function (a, b) { // sort rows
        return reverse // `-1 *` if want opposite order
            * (a.cells[col].textContent.trim() // using `.textContent.trim()` for test
                .localeCompare(b.cells[col].textContent.trim())
               );
    });
    for(i = 0; i < tr.length; ++i) tb.appendChild(tr[i]); // append each row in order
}
// sortTable(tableNode, columId, false);

If you don't want to make the assumptions above, you'd need to consider how you want to behave in each circumstance. (e.g. put everything into one <tbody> or add up all the preceeding colspan values, etc.)

You could then attach this to each of your tables, e.g. assuming titles are in <thead>

function makeSortable(table) {
    var th = table.tHead, i;
    th && (th = th.rows[0]) && (th = th.cells);
    if (th) i = th.length;
    else return; // if no `<thead>` then do nothing
    while (--i >= 0) (function (i) {
        var dir = 1;
        th[i].addEventListener('click', function () {sortTable(table, i, (dir = 1 - dir))});
    }(i));
}

function makeAllSortable(parent) {
    parent = parent || document.body;
    var t = parent.getElementsByTagName('table'), i = t.length;
    while (--i >= 0) makeSortable(t[i]);
}

and then invoking makeAllSortable onload.


Example fiddle of it working on a table.

Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • Ah, my mistake with the hiding table. Just trying this out now. Just to clarify, I'd be adding the `makeAllSortable()` to the body onload correct? – lukeseager Jan 10 '13 at 22:33
  • Yep, re-copy the code as it stands now though because I edited it. Also ensure you understand the assumptions I made. – Paul S. Jan 10 '13 at 22:34
  • @lucas572 it currently only makes a decision based upon `.textContent.trim()` (which doesn't change based on checkbox state, etc). If you want to to decide in other ways, teach it how in `.sort`. – Paul S. Jan 10 '13 at 23:46
  • @PaulS. thank you very much, very useful. I tested it on all browsers, and it worked just fine, except for IE10, do you have any idea why? – user2517028 Mar 05 '14 at 10:57
  • @user2517028 using _IE11_ to emulate older versions, it works for me until IE8 where `addEventListener` fails (and `String.prototype.trim` would fail too). To try and find out what went wrong, press F12 and take a look at what errors appear/ed in the _Console_. – Paul S. Mar 05 '14 at 11:40
  • Thanks, nice code. Any idea how to make it works with numbers? Because actually 9 is considered bigger than 42 for example. I'd like to avoid to pad the numbers if possible... – Fla Feb 14 '17 at 20:11
  • Also, why do you need to trim() the strings? – Fla Feb 14 '17 at 20:21
  • @Fla trim because whitespace can change expected behaviour. If you want string-or-number etc, google natural sort – Paul S. Feb 14 '17 at 20:28
  • 13
    Answer to myself, I find in [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare#Using_options) the usage of options, so use the `numeric: true` one and you'll be good with numbers, even inside strings. Replace `.localeCompare(b.cells[col].textContent.trim())` by `.localeCompare(b.cells[col].textContent.trim(), undefined, {numeric: true})` – Fla Feb 14 '17 at 20:34
  • @PaulS. Hi, I have a question about sorting number. There are positive and negative numbers in my table and this script this script wrongly sorts negative numbers from positive Look at this example: jsfiddle.net/zscQy/775 – michal Jan 29 '18 at 20:50
65

Nick Grealy's accepted answer is great but acts a bit quirky if your rows are inside a <tbody> tag (the first row isn't ever sorted and after sorting rows end up outside of the tbody tag, possibly losing formatting).

This is a simple fix, however:

Just change:

document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
  const table = th.closest('table');
  Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
    .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
    .forEach(tr => table.appendChild(tr) );

to:

document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
  const table = th.closest('table');
  const tbody = table.querySelector('tbody');
  Array.from(tbody.querySelectorAll('tr'))
    .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
    .forEach(tr => tbody.appendChild(tr) );
jedwards
  • 29,432
  • 3
  • 65
  • 92
  • 4
    Excellent fix! I was having issues because my table had `` and this fixed it right up – T-101 Mar 15 '21 at 15:51
  • You've absolutely saved me! I was trying to fix the same thing without any luck. This is very clean solution to tbody tag / fixed first row issue. – k0rnik Aug 14 '21 at 21:21
  • If I've used formatters in to_html, my float numbers has been converted to string, which makes above sorting function not working correctly. Is it possible to upgrade the code so its converts string resulting from lambda x: '{0:.3f}%'.format(x*100) back to float just to sort it correctly? – k0rnik Aug 14 '21 at 22:35
  • I can fix string formatting of the float by using parseFloat in this case I've amended 1 of your lines: (parseFloat(getCellValue(asc ? a : b, idx)), parseFloat(getCellValue(asc ? b : a, idx)). The problem is that for non-float_as_string columns parseFloat will return NaN which won't allow for sorting. I am struggling to fix your code so it can check isNaN for parseFloat before parseing. – k0rnik Aug 15 '21 at 12:58
  • This works for all fields except my date field. Any ideas why it would not work with a date field? – user3520445 Feb 16 '22 at 19:27
8

The best way I know to sort HTML table with javascript is with the following function.

Just pass to it the id of the table you'd like to sort and the column number on the row. it assumes that the column you are sorting is numeric or has numbers in it and will do regex replace to get the number itself (great for currencies and other numbers with symbols in it).

function sortTable(table_id, sortColumn){
    var tableData = document.getElementById(table_id).getElementsByTagName('tbody').item(0);
    var rowData = tableData.getElementsByTagName('tr');            
    for(var i = 0; i < rowData.length - 1; i++){
        for(var j = 0; j < rowData.length - (i + 1); j++){
            if(Number(rowData.item(j).getElementsByTagName('td').item(sortColumn).innerHTML.replace(/[^0-9\.]+/g, "")) < Number(rowData.item(j+1).getElementsByTagName('td').item(sortColumn).innerHTML.replace(/[^0-9\.]+/g, ""))){
                tableData.insertBefore(rowData.item(j+1),rowData.item(j));
            }
        }
    }
}

Using example:

$(function(){
    // pass the id and the <td> place you want to sort by (td counts from 0)
    sortTable('table_id', 3);
});
AfikDeri
  • 2,069
  • 17
  • 19
  • I like this solution, but isn't it faster to use the sort() ? – Martin Zvarík Nov 13 '19 at 00:29
  • Why do you need to place an anonymous function in a jQuery thing just to call your function? Couldn't you simply call `sortTable('table_id', 3)` directly? – payne Oct 23 '20 at 20:59
7

It does WAY more than "just sorting", but dataTables.net does what you need. I use it daily and is well supported and VERY fast (does require jQuery)

http://datatables.net/

DataTables is a plug-in for the jQuery Javascript library. It is a highly flexible tool, based upon the foundations of progressive enhancement, which will add advanced interaction controls to any HTML table.

Google Visualizations is another option, but requires a bit more setup that dataTables, but does NOT require any particular framework/library (other than google.visualizations):

http://code.google.com/apis/ajax/playground/?type=visualization#table

And there are other options to... especially if you're using one of the other JS frameworks. Dojo, Prototype, etc all have usable "table enhancement" plugins that provide at minimum table sorting functionality. Many provide more, but I'll restate...I've yet to come across one as powerful and as FAST as datatables.net.

BLSully
  • 5,929
  • 1
  • 27
  • 43
  • 1
    hmm I'll take a look at that. Was hoping for a javascript solution though, but hey if this works! My only worry with some is that they get confused when there's HTML in the table, will this just read through the HTML and not really care what it's looking at? Or will it try to ignore it? – lukeseager Jan 10 '13 at 22:04
  • Well, technically it is JavaScript. jQuery is nothing but a JS framework...not magic. DataTables is pretty good about dealing with some HTML, however, you may need to write a custom sorter (documented on his website) – BLSully Jan 10 '13 at 22:10
  • 4
    @BLSully : what about http://listjs.com/ ? I read that datatables was quite chunky, a big canon for shooting small birds in some cases – Adriano Apr 14 '14 at 15:27
  • @AdrienBe: Nice suggestion! Haven't run across that one before. Will have to see how it does with really large data sets. I've run 15,000+ rows in DataTables before and it's performance remains acceptable in modern browsers. But I will definitely agree that DataTables can be too large of a hammer for some problems. ListJS looks like a great alternative (and also lets you use html other than tables) – BLSully Apr 15 '14 at 14:15
  • @BLSully: been using it today and it seems alright, here is a very simple example to get you started http://stackoverflow.com/questions/20528593/using-list-js-sort-and-search-not-working/23078200#23078200 – Adriano Apr 15 '14 at 16:51
6

Table Sorting with :hover arrows effect. Simply add the class .order to the <th> element of each column to be ordered

function table_sort() {
  const styleSheet = document.createElement('style')
  styleSheet.innerHTML = `
        .order-inactive span {
            visibility:hidden;
        }
        .order-inactive:hover span {
            visibility:visible;
        }
        .order-active span {
            visibility: visible;
        }
    `
  document.head.appendChild(styleSheet)

  document.querySelectorAll('th.order').forEach(th_elem => {
    let asc = true
    const span_elem = document.createElement('span')
    span_elem.style = "font-size:0.8rem; margin-left:0.5rem"
    span_elem.innerHTML = "▼"
    th_elem.appendChild(span_elem)
    th_elem.classList.add('order-inactive')

    const index = Array.from(th_elem.parentNode.children).indexOf(th_elem)
    th_elem.addEventListener('click', (e) => {
      document.querySelectorAll('th.order').forEach(elem => {
        elem.classList.remove('order-active')
        elem.classList.add('order-inactive')
      })
      th_elem.classList.remove('order-inactive')
      th_elem.classList.add('order-active')

      if (!asc) {
        th_elem.querySelector('span').innerHTML = '▲'
      } else {
        th_elem.querySelector('span').innerHTML = '▼'
      }
      const arr = Array.from(th_elem.closest("table").querySelectorAll('tbody tr'))
      arr.sort((a, b) => {
        const a_val = a.children[index].innerText
        const b_val = b.children[index].innerText
        return (asc) ? a_val.localeCompare(b_val) : b_val.localeCompare(a_val)
      })
      arr.forEach(elem => {
        th_elem.closest("table").querySelector("tbody").appendChild(elem)
      })
      asc = !asc
    })
  })
}

table_sort()
<table>
  <thead>
    <tr>
      <th class="order">Country</th>
      <th class="order">Date</th>
      <th class="order">Size</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>France</td>
      <td>2001-01-01</td>
      <td><i>25</i></td>
    </tr>
    <tr>
      <td><a href=#>spain</a></td>
      <td><i>2005-05-05</i></td>
      <td></td>
    </tr>
    <tr>
      <td><b>Lebanon</b></td>
      <td><a href=#>2002-02-02</a></td>
      <td><b>-17</b></td>
    </tr>
    <tr>
      <td><i>Argentina</i></td>
      <td>2005-04-04</td>
      <td><a href=#>100</a></td>
    </tr>
    <tr>
      <td>USA</td>
      <td></td>
      <td>-6</td>
    </tr>
  </tbody>
</table>
Cesar Morillas
  • 707
  • 5
  • 11
  • Worked great after fixing the heading being sorted. Used: `const arr = Array.from(th_elem.closest("table").querySelectorAll('tbody tr')).slice(1)` – WinEunuuchs2Unix May 28 '22 at 21:50
  • Querying `closest` table and its tbody in a loop is inefficient. You should cache `tbody` to a variable. Also you can replace `appendChild`in a loop with single call of `replaceChildren`: `tbody.replaceChildren(...arr)`. – JukkaP Jul 03 '23 at 14:36
4

You could deal with a json array and the sort function. It is a pretty easy maintanable structure to manipulate (ex: sorting).

Untested, but here's the idea. That would support multiple ordering and sequential ordering if you pass in a array in which you put the columns in the order they should be ordered by.

var DATA_TABLE = {
    {name: 'George', lastname: 'Blarr', age:45},
    {name: 'Bob', lastname: 'Arr', age: 20}
    //...
};

function sortDataTable(arrayColNames, asc) { // if not asc, desc
    for (var i=0;i<arrayColNames.length;i++) {
        var columnName = arrayColNames[i];
        DATA_TABLE = DATA_TABLE.sort(function(a,b){
            if (asc) {
                return (a[columnName] > b[columnName]) ? 1 : -1;
            } else {
                return (a[columnName] < b[columnName]) ? 1 : -1;
            }
        });
    }
}

function updateHTMLTable() {
    // update innerHTML / textContent according to DATA_TABLE
    // Note: textContent for firefox, innerHTML for others
}

Now let's imagine you need to order by lastname, then name, and finally by age.

var orderAsc = true;
sortDataTable(['lastname', 'name', 'age'], orderAsc);

It should result in something like :

{name: 'Jack', lastname: 'Ahrl', age: 20},
{name: 'Jack', lastname: 'Ahrl', age: 22},
//...
Frederik.L
  • 5,522
  • 2
  • 29
  • 41
  • I have a fairly long table of ~200 rows and growing. Do you think this method would be faster than the other answers? – posfan12 Mar 30 '19 at 19:56
4

Here is a complete example using pure JavaScript. The algorithm used for sorting is basically BubbleSort. Here is a Fiddle.

   <!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">

<script type="text/javascript">
    function sort(ascending, columnClassName, tableId) {
        var tbody = document.getElementById(tableId).getElementsByTagName(
                "tbody")[0];
        var rows = tbody.getElementsByTagName("tr");

        var unsorted = true;

        while (unsorted) {
            unsorted = false

            for (var r = 0; r < rows.length - 1; r++) {
                var row = rows[r];
                var nextRow = rows[r + 1];

                var value = row.getElementsByClassName(columnClassName)[0].innerHTML;
                var nextValue = nextRow.getElementsByClassName(columnClassName)[0].innerHTML;

                value = value.replace(',', '.'); // in case a comma is used in float number
                nextValue = nextValue.replace(',', '.');

                if (!isNaN(value)) {
                    value = parseFloat(value);
                    nextValue = parseFloat(nextValue);
                }

                if (ascending ? value > nextValue : value < nextValue) {
                    tbody.insertBefore(nextRow, row);
                    unsorted = true;
                }
            }
        }
    };
</script>
</head>
<body>
    <table id="content-table">
        <thead>
            <tr>
                <th class="id">ID <a
                    href="javascript:sort(true, 'id', 'content-table');">asc</a> <a
                    href="javascript:sort(false, 'id', 'content-table');">des</a>
                </th>
                <th class="country">Country <a
                    href="javascript:sort(true, 'country', 'content-table');">asc</a> <a
                    href="javascript:sort(false, 'country', 'content-table');">des</a>
                </th>
                <th class="some-fact">Some fact <a
                    href="javascript:sort(true, 'some-fact', 'content-table');">asc</a>
                    <a href="javascript:sort(false, 'some-fact', 'content-table');">des</a>
                <th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td class="id">001</td>
                <td class="country">Germany</td>
                <td class="some-fact">16.405</td>
            </tr>
            <tr>
                <td class="id">002</td>
                <td class="country">France</td>
                <td class="some-fact">10.625</td>
            </tr>
            <tr>
                <td class="id">003</td>
                <td class="country">UK</td>
                <td class="some-fact">15.04</td>
            </tr>
            <tr>
                <td class="id">004</td>
                <td class="country">China</td>
                <td class="some-fact">13.536</td>
            </tr>
        </tbody>
    </table>
</body>
</html>

You can also check out the source from here: https://github.com/wmentzel/table-sort

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
3

Sorting table rows by cell. 1. Little simpler and has some features. 2. Distinguish 'number' and 'string' on sorting 3. Add toggle to sort by ASC, DESC

var index;      // cell index
var toggleBool; // sorting asc, desc 
function sorting(tbody, index){
    this.index = index;
    if(toggleBool){
        toggleBool = false;
    }else{
        toggleBool = true;
    }

    var datas= new Array();
    var tbodyLength = tbody.rows.length;
    for(var i=0; i<tbodyLength; i++){
        datas[i] = tbody.rows[i];
    }

    // sort by cell[index] 
    datas.sort(compareCells);
    for(var i=0; i<tbody.rows.length; i++){
        // rearrange table rows by sorted rows
        tbody.appendChild(datas[i]);
    }   
}

function compareCells(a,b) {
    var aVal = a.cells[index].innerText;
    var bVal = b.cells[index].innerText;

    aVal = aVal.replace(/\,/g, '');
    bVal = bVal.replace(/\,/g, '');

    if(toggleBool){
        var temp = aVal;
        aVal = bVal;
        bVal = temp;
    } 

    if(aVal.match(/^[0-9]+$/) && bVal.match(/^[0-9]+$/)){
        return parseFloat(aVal) - parseFloat(bVal);
    }
    else{
          if (aVal < bVal){
              return -1; 
          }else if (aVal > bVal){
                return 1; 
          }else{
              return 0;       
          }         
    }
}

below is html sample

            <table summary="Pioneer">

                <thead>
                    <tr>
                        <th scope="col"  onclick="sorting(tbody01, 0)">No.</th>
                        <th scope="col"  onclick="sorting(tbody01, 1)">Name</th>
                        <th scope="col"  onclick="sorting(tbody01, 2)">Belong</th>
                        <th scope="col"  onclick="sorting(tbody01, 3)">Current Networth</th>
                        <th scope="col"  onclick="sorting(tbody01, 4)">BirthDay</th>
                        <th scope="col"  onclick="sorting(tbody01, 5)">Just Number</th>
                    </tr>
                </thead>

                <tbody id="tbody01">
                    <tr>
                        <td>1</td>
                        <td>Gwanshic Yi</td>
                        <td>Gwanshic Home</td>
                        <td>120000</td>
                        <td>1982-03-20</td>
                        <td>124,124,523</td>
                    </tr>
                    <tr>
                        <td>2</td>
                        <td>Steve Jobs</td>
                        <td>Apple</td>
                        <td>19000000000</td>
                        <td>1955-02-24</td>
                        <td>194,523</td>
                    </tr>
                    <tr>
                        <td>3</td>
                        <td>Bill Gates</td>
                        <td>MicroSoft</td>
                        <td>84300000000</td>
                        <td>1955-10-28</td>
                        <td>1,524,124,523</td>
                    </tr>
                    <tr>
                        <td>4</td>
                        <td>Larry Page</td>
                        <td>Google</td>
                        <td>39100000000</td>
                        <td>1973-03-26</td>
                        <td>11,124,523</td>
                    </tr>
                </tbody>
            </table>

Gwanshic
  • 31
  • 2
2

Another compact but readable solution: It just requires adding the class .order to the <th> element of each column to be ordered

document.querySelectorAll('th.order').forEach(th_elem => {
    let asc=true
    const index = Array.from(th_elem.parentNode.children).indexOf(th_elem)          
    th_elem.addEventListener('click', (e) => {              
        const arr = [... th_elem.closest("table").querySelectorAll('tbody tr')]
        arr.sort( (a, b) => {
            const a_val = a.children[index].innerText
            const b_val = b.children[index].innerText                   
            return (asc) ? a_val.localeCompare(b_val) : b_val.localeCompare(a_val)
        })
        arr.forEach(elem => {                   
            th_elem.closest("table").querySelector("tbody").appendChild(elem)
        })
        asc = !asc
    })
})
Cesar Morillas
  • 707
  • 5
  • 11
  • 1
    thanks a lot for this beautiful solution! I just had to slice off the first line of the table to prevent the TH from being sorted... const arr = [... th_elem.closest("table").querySelectorAll('tbody tr')].slice(1) – T.M. Dec 15 '21 at 10:34
1

In case your table does not have ths but only tds (with headers included) you can try the following which is based on Nick Grealy's answer above:

const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;

const comparer = (idx, asc) => (a, b) => ((v1, v2) => 
    v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
    )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

// do the work...
document.querySelectorAll('tr:first-child td').forEach(td => td.addEventListener('click', (() => {
    const table = td.closest('table');
    Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(comparer(Array.from(td.parentNode.children).indexOf(td), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
})));
@charset "UTF-8";
@import url('https://fonts.googleapis.com/css?family=Roboto');

*{
  font-family: 'Roboto', sans-serif;
  text-transform:capitalize;
  overflow:hidden;
  margin: 0 auto;
  text-align:left;
}

table {
 color:#666;
 font-size:12px;
 background:#124;
 border:#ccc 1px solid;
 -moz-border-radius:3px;
 -webkit-border-radius:3px;
 border-radius:3px;
  border-collapse: collapse;
  width: 100%;
}

table td {
 padding:10px;
 border-top: 1px solid #ffffff;
 border-bottom:1px solid #e0e0e0;
 border-left: 1px solid #e0e0e0;
 background: #fafafa;
 background: -webkit-gradient(linear, left top, left bottom, from(#fbfbfb), to(#fafafa));
 background: -moz-linear-gradient(top,  #fbfbfb,  #fafafa);
  width: 6.9in;
}

table tbody tr:first-child td
{
 background: #124!important;
  color:#fff;
}

table tbody tr th
{
  padding:10px;
  border-left: 1px solid #e0e0e0;
 background: #124!important;
  color:#fff;
}
<table>
        <tr><td>Country</td><td>Date</td><td>Size</td></tr>
        <tr><td>France</td><td>2001-01-01</td><td><i>25</i></td></tr>
        <tr><td>spain</td><td>2005-05-05</td><td></td></tr>
        <tr><td>Lebanon</td><td>2002-02-02</td><td><b>-17</b></td></tr>
        <tr><td>Argentina</td><td>2005-04-04</td><td>100</td></tr>
        <tr><td>USA</td><td></td><td>-6</td></tr>
    </table>
Datacrawler
  • 2,780
  • 8
  • 46
  • 100
1

Another approach to sort HTML table. (based on W3.JS HTML Sort)

var collection = [{
  "Country": "France",
  "Date": "2001-01-01",
  "Size": "25",
}, {
  "Country": "spain",
  "Date": "2005-05-05",
  "Size": "",
}, {
  "Country": "Lebanon",
  "Date": "2002-02-02",
  "Size": "-17",
}, {
  "Country": "Argentina",
  "Date": "2005-04-04",
  "Size": "100",
}, {
  "Country": "USA",
  "Date": "",
  "Size": "-6",
}]

for (var j = 0; j < 3; j++) {
  $("#myTable th:eq(" + j + ")").addClass("control-label clickable");
  $("#myTable th:eq(" + j + ")").attr('onClick', "w3.sortHTML('#myTable', '.item', 'td:nth-child(" + (j + 1) + ")')");
}

$tbody = $("#myTable").append('<tbody></tbody>');

for (var i = 0; i < collection.length; i++) {
  $tbody = $tbody.append('<tr class="item"><td>' + collection[i]["Country"] + '</td><td>' + collection[i]["Date"] + '</td><td>' + collection[i]["Size"] + '</td></tr>');
}
.control-label:after {
  content: "*";
  color: red;
}

.clickable {
  cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://www.w3schools.com/lib/w3.js"></script>
<link href="https://www.w3schools.com/w3css/4/w3.css" rel="stylesheet" />
<p>Click the <strong>table headers</strong> to sort the table accordingly:</p>

<table id="myTable" class="w3-table-all">
  <thead>
    <tr>
      <th>Country</th>
      <th>Date</th>
      <th>Size</th>
    </tr>
  </thead>
</table>
Penny Liu
  • 15,447
  • 5
  • 79
  • 98
0

Sorting html table column on page load

var table = $('table#all_items_table');
var rows = table.find('tr:gt(0)').toArray().sort(comparer(3));
for (var i = 0; i < rows.length; i++) {
    table.append(rows[i])
}
function comparer(index) {
    return function (a, b) {
        var v1= getCellValue(a, index),
        v2= getCellValue(b, index);
        return $.isNumeric(v2) && $.isNumeric(v1) ? v2 - v1: v2.localeCompare(v1)
    }
}


function getCellValue(row, index) {
    return parseFloat($(row).children('td').eq(index).html().replace(/,/g,'')); //1234234.45645->1234234
}

enter image description here

executable
  • 3,365
  • 6
  • 24
  • 52
0

I have edited the code from one of the example here to use jquery. It's still not 100% jquery though. Any thoughts on the two different versions, like what are the pros and cons of each?

function column_sort() {
    getCellValue = (tr, idx) => $(tr).find('td').eq( idx ).text();

    comparer = (idx, asc) => (a, b) => ((v1, v2) => 
        v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
        )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
    
    table = $(this).closest('table')[0];
    tbody = $(table).find('tbody')[0];

    elm = $(this)[0];
    children = elm.parentNode.children;
    Array.from(tbody.querySelectorAll('tr')).sort( comparer(
        Array.from(children).indexOf(elm), table.asc = !table.asc))
        .forEach(tr => tbody.appendChild(tr) );
}

table.find('thead th').on('click', column_sort);
Daniel Huckson
  • 1,157
  • 1
  • 13
  • 35
0

<!DOCTYPE html>
<html>
<head>
<style>
  table, td, th {
    border: 1px solid;
    border-collapse: collapse;
  }
  td , th {
    padding: 5px;
    width: 100px;
  }
  th {
    background-color: lightgreen;
  }
</style>
</head>
<body>

<h2>JavaScript Array Sort</h2>

<p>Click the buttons to sort car objects on age.</p>

<p id="demo"></p>

<script>
var nameArrow = "", yearArrow = "";
var cars = [
  {type:"Volvo", year:2016},
  {type:"Saab", year:2001},
  {type:"BMW", year:2010}
];
yearACS = true;
function sortYear() {
  if (yearACS) {
    nameArrow = "";
    yearArrow = "";
    cars.sort(function(a,b) {
      return a.year - b.year;
    });
    yearACS = false;
  }else {
    nameArrow = "";
    yearArrow = "";
    cars.sort(function(a,b) {
      return b.year - a.year;
    });
    yearACS = true;
  }
  displayCars();
}
nameACS = true;
function sortName() {
  if (nameACS) {
    nameArrow = "";
    yearArrow = "";
    cars.sort(function(a,b) {
      x = a.type.toLowerCase();
      y = b.type.toLowerCase();
      if (x > y) {return 1;}
      if (x < y) {return -1};
      return 0;
    });
    nameACS = false;
  } else {
    nameArrow = "";
    yearArrow = "";
    cars.sort(function(a,b) {
      x = a.type.toUpperCase();
      y = b.type.toUpperCase();
      if (x > y) { return -1};
      if (x <y) { return 1 };
      return 0;
    });
    nameACS = true;
  }
  displayCars();
}

displayCars();

function displayCars() {
  var txt = "<table><tr><th onclick='sortName()'>name " + nameArrow + "</th><th onclick='sortYear()'>year " + yearArrow + "</th><tr>";
  for (let i = 0; i < cars.length; i++) {
    txt += "<tr><td>"+ cars[i].type + "</td><td>" + cars[i].year + "</td></tr>";
  }
  txt += "</table>";
  document.getElementById("demo").innerHTML = txt;
}

</script>

</body>
</html>
  • 1
    A link to a solution is welcome, but please ensure your answer is useful without it: [add context around the link](//meta.stackexchange.com/a/8259) so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. [Answers that are little more than a link may be deleted.](/help/deleted-answers) – Steve Dec 09 '20 at 16:16
0

I'm very grateful for the accepted answer and jedwards' fix, but I also find them poorly readable. So here's my refactored and verbose version:

// Remember that strings are false positives for isNaN
const isEmptyOrNaN = (obj) => obj === "" || isNaN(obj);

const getCellValueInColumn = (tr, columnIdx) =>
  tr.children[columnIdx].innerText || tr.children[columnIdx].textContent;

const compareCellValues = (cellValue1, cellValue2) => {
  return isEmptyOrNaN(cellValue1) || isEmptyOrNaN(cellValue2)
    ? cellValue1.toString().localeCompare(cellValue2)
    : cellValue1 - cellValue2;
};

const compareFnFactory = (columnIdx, ascending) => (firstEl, secondEl) => {
  const cellValue1 = getCellValueInColumn(firstEl, columnIdx);
  const cellValue2 = getCellValueInColumn(secondEl, columnIdx);
  return ascending
    ? compareCellValues(cellValue1, cellValue2)
    : compareCellValues(cellValue2, cellValue1);
};

document.querySelectorAll("th").forEach((th) =>
  th.addEventListener("click", () => {
    const table = th.closest("table");
    const tbody = table.querySelector("tbody");
    const columnIdx = Array.from(th.parentNode.children).indexOf(th);
    const compareFn = compareFnFactory(columnIdx, (this.ascending = !this.ascending));
    Array.from(tbody.querySelectorAll("tr"))
      .sort(compareFn)
      .forEach((tr) => tbody.appendChild(tr));
  })
);

If you find any extra spaces or parenthesis, unnecessary indents, etc., it's because I've formatted the code with prettier.

I've wrapped this code inside a:

javascript: (function () {
  // Code here
})();

and put it into a bookmarklet, so now I can sort columns inside Keycloak Admin Console.

vault
  • 3,930
  • 1
  • 35
  • 46
0

I've found myself using @NickGrealy method to sort items and it works great! (https://stackoverflow.com/a/49041392/18045902)

Issue I had is that i'm using a different format for date: dd-mm-YY instead of the ISO one.

As I'm passing data from a .php file as a string I had to convert the string in a date then compare with ><==

Substitute the compare function

// Returns a function responsible for sorting a specific column index 
// (idx = columnIndex, asc = ascending order?).
var comparer = function(idx, asc) { 

// This is used by the array.sort() function...
return function(a, b) { 

    // This is a transient function, that is called straight away. 
    // It allows passing in different order of args, based on 
    // the ascending/descending order.
    return function(v1, v2) {

        // sort based on a numeric or localeCompare, based on type...
        return (v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)) 
            ? v1 - v2 
            : v1.toString().localeCompare(v2);
    }(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
}

};

with this:

    var comparer = function(idx, asc) { 

// This is used by the array.sort() function...
return function(a, b) { 

    // This is a transient function, that is called straight away. 
    // It allows passing in different order of args, based on 
    // the ascending/descending order.
    return function(v1, v2) {
        
        if(v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)){
            x = v1 - v2;
            console.log(v1); 
        } else if(v1.includes("-")) {
            var partsArray1 = v1.split('-');
            var partsArray2 = v2.split('-');
            var data1 = new Date(partsArray1[2],partsArray1[1],partsArray1[0]);
            var data2 = new Date(partsArray2[2],partsArray2[1],partsArray2[0]);
            if(data1>data2){
                x=1;
            } else if (data1<data2) {
                x=-1;
            } else if (data1==data2) {
                x=0;
            }
            

        } else {
            x = v1.toString().localeCompare(v2);
        }
        // sort based on a numeric or localeCompare, based on type...
        //return (v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)) 
        //    ? v1 - v2 
        //    : v1.toString().localeCompare(v2);

        return x;

    }(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
}
};

NOTE: this will only work if the date you are trying to parse is a string in the dd-mm-YY format. If you need a different format change the includes() and the split() character (in my case is "-") and the order of the date you are creating with Date().

If there's something wrong with this method please comment.

giovnn
  • 21
  • 5
0

Here is my version, where I have cached table, tbody, rows, and cellIndex for better performance. I also make use of replaceChildrento avoid using appendChild in a loop.


for (const table of document.getElementsByTagName("table")) {
  const tBody = table.tBodies[0];
  const rows = Array.from(tBody.rows);
  for (const th of table.tHead.rows[0].cells) {
    th.addEventListener("click", e => {
      const cellIndex = e.target.cellIndex;
      rows.sort((a, b) => a.cells[cellIndex].textContent.trim().localeCompare(b.cells[cellIndex].textContent.trim()));
      tBody.replaceChildren(...rows);
    });
  }
}

In the html table markup, for code to work, there should be exactly one thead element wrapping the th elements, and zero or at most one tbody element wrapping the tdelements, since tbodycan always be omitted according to the html standards. (Every table element has implicitly exactly one tbody element unless explicitly specified more.)

JukkaP
  • 1,994
  • 1
  • 14
  • 7