143

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?

0

20 Answers 20

302

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));
    }
};
Sign up to request clarification or add additional context in comments.

29 Comments

Note that the statement th.parentNode.parentNode fails if the header row is wrapped in a <thead> element. I'd suggest using "var table = th.closest('table');". Note that Element.closest() doesn't seem to be implemented in IE11/Edge.
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 <script> tag, nothing happens. You need to link this function to some event, for example to load window: window.onload=function(){ /*script from this answer */} and then it works fine!
@NickGrealy Thanks for graceful code! What is this designation called: this.asc = !this.asc
If your rows are inside a <tbody> tag and this answer acts up, see this tweak to this answer.
Very nice! I had to check 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".
|
89

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.

10 Comments

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?
Yep, re-copy the code as it stands now though because I edited it. Also ensure you understand the assumptions I made.
@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.
@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?
Answer to myself, I find in MDN 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})
|
76

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) );

5 Comments

Excellent fix! I was having issues because my table had <tbody> and this fixed it right up
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.
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?
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.
This works for all fields except my date field. Any ideas why it would not work with a date field?
10

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>

2 Comments

Worked great after fixing the heading being sorted. Used: const arr = Array.from(th_elem.closest("table").querySelectorAll('tbody tr')).slice(1)
Querying closest table and its tbody in a loop is inefficient. You should cache tbody to a variable. Also you can replace appendChildin a loop with single call of replaceChildren: tbody.replaceChildren(...arr).
8

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.

5 Comments

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?
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 : what about listjs.com ? I read that datatables was quite chunky, a big canon for shooting small birds in some cases
@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: been using it today and it seems alright, here is a very simple example to get you started stackoverflow.com/questions/20528593/…
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);
});

2 Comments

I like this solution, but isn't it faster to use the sort() ?
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?
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},
//...

1 Comment

I have a fairly long table of ~200 rows and growing. Do you think this method would be faster than the other answers?
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

Comments

4

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
    })
})

2 Comments

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)
Works for me as is.
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>

Comments

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>

Comments

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>

Comments

1

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.

Comments

1

Here is the accepted answer as a complete html file, with script tag, style tag, and wrapped in window.onload function.
Just copy the text, paste it to notepad and save as page.html .
You could also add <head> before <script>, and closing tag </head> after </style> . But copying it as it is will also work. Tested it in firefox/windows.
Thanks to Nick Grealy, and Vladimir.
Note that first row in the table, has <th> (table header), not <td> (table data) tag.

    <script>
    window.onload=function(){ 
    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) );
    })));
    
    }
    </script>
    <style>
    table, th, td {
        border: 1px solid black;
    }
    th {
        cursor: pointer;
    }
    </style>
    <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>

If you wish to add some color, and alternating rows color, use this as css style:

    <style>
        table {
            border-collapse: collapse;

            /* width: 100%; */
        }
          
        th, td {
            text-align: left;
            padding: 8px;
        border: 1px solid green;
        }

    th {
            cursor: pointer;
            text-decoration: underline;
    }   
          
        tr:nth-child(even) {
            background-color: Beige;
        }
    </style>

Comments

1

I took the freedom to enhance Nick Grealy´s answer with automated suffixing to display the sort order.

I also fixed a small bug I discovered after I got the suffixing working: When clicking on a column, that is sorted ascending. When then clicking on another column, that is sorted descending (in my Waterfox, at least). this.asc seems to be stored staticly in the function, not in the element. The patch uses a custom attribute on the th element, which is set for ascending and removed for descending.

Simply insert these lines right after the const definitions:

    var asc = th.hasAttribute('sortAsc');
    th.closest('tr').querySelectorAll('th').forEach(th => {
        th.classList.remove('sort-asc');
        th.classList.remove('sort-desc');
        th.removeAttribute('sortAsc');
    });
    th.classList.add(asc=!asc?'sort-asc':'sort-desc');
    if (asc) th.setAttribute('sortAsc', '1');

and replace this.asc = !this.asc with asc.

the CSS:

.sort-asc:after { content: ' ^'; } .sort-desc:after { content: ' v' }

Another idea was to reflect the stable sort: This variant does not remove the sort order from the previously sorted columns, but merely adds another CSS class to highlight the latest sort:

    var asc = !th.hasAttribute('sortAsc');
    th.classList.remove(asc?'sort-desc':'sort-asc');
    th.classList.add(asc?'sort-asc':'sort-desc');
    th.closest('tr').querySelectorAll('th').forEach(th => { th.classList.remove('sort-current'); });
    th.classList.add('sort-current');
    asc ? th.setAttribute('sortAsc', 1) : th.removeAttribute('sortAsc');

plus additional CSS:

.sort-current { background-color:green; }

1 Comment

Since almost half of the answers now are relating to Nick Grealy´s answer, I wanted to add titles so anyone can find stuff faster. But I discarded that idea when I found that the <h1> and <h2> from the markdown are more prominent than the actual page header (<h1 class="fs-headline1">).
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

Comments

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 Comment

A link to a solution is welcome, but please ensure your answer is useful without it: add context around the link 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.
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);

Comments

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.

Comments

0

A combination of answers Nick Grealy, jedwards and vault with some changes:

  1. Works on Chrome version 30.0.0.0 (released 2013-12-09) which is used in Android 4.4 WebView (I needed it)
  2. Column sorting is set for all tables on the page
  3. Supported tables with tbody and without it
  4. th and td headers are supported (although it seems to have already worked)
// use setTableSortCallbacks();

function setTableSortCallbacks() {
    const tables = document.querySelectorAll("table");
    for (var i = 0; i < tables.length; i++) {
        const table = tables[i];
        if (table.rows.length > 0) {
            const cells = table.rows[0].cells;
            for (var j = 0; j < cells.length; j++) {
                setCellCallback(table, cells[j]);
            }
        }
    }
}

function setCellCallback(table, cell) {
    cell.onclick = function() {
        sortTableByColumn(table, cell);
    };
}

function sortTableByColumn(table, cell) {
    const tbody = table.querySelector("tbody");
    const parent = (tbody != null) ? tbody : table;

    const rows = parent.querySelectorAll('tr:nth-child(n+2)');

    const rowsArray = toArray(rows);
    const compareFn = compareRowsFnFactory(cell.cellIndex, this.asc = !this.asc);
    rowsArray.sort(compareFn);
    for (var i = 0; i < rowsArray.length; i++) {
        parent.appendChild(rowsArray[i]);
    }
}

function compareRowsFnFactory(colIndex, asc) {
    return function(a, b) {
        const cellValue1 = getCellValue(a, colIndex);
        const cellValue2 = getCellValue(b, colIndex);
        return asc
            ? compareCellValues(cellValue1, cellValue2)
            : compareCellValues(cellValue2, cellValue1);
    };
}

function getCellValue(row, colIndex) {
    const cell = row.children[colIndex];
    return cell.innerText || cell.textContent;
}

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

function isEmptyOrNaN(value) {
    return value === "" || isNaN(value);
}

function toArray(nodesList) {
    var arr = [];
    for(var i = 0, node; node = nodesList[i]; ++i) {
        arr.push(node);
    }
    return arr;
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.