Description
Table rows must be swapped at arbitrary positions in the table, i.e. row i and row j must change positions, where i and j are not necessarily adjacent. See current implementation below. Table rows should be sorted by column; a column is specified by sort_index
which is generated by pressing one of the table headers.
The problem with the implementation is that the table is sorted incrementally by each click of a header, while it should be sorted by a single click.
var table_index = {
watched: 0,
title: 1,
director: 2,
year: 3
};
var sort_index = 0;
$(document).ready(function()
{
var table_headers = document.getElementById("header-item").children;
for (var k = 0; k < table_headers.length; k++)
{
$("#" + table_headers[k].id).bind("click", function(e)
{
sort_index = table_index[e.target.id];
var table = document.getElementById("film-list");
for (var i = 1; i < table.rows.length - 1; i++)
{
var a = table.rows[i].getElementsByTagName("td")[sort_index].innerHTML;
for (var j = i + 1; j < table.rows.length; j++)
{
var b = table.rows[j].getElementsByTagName("td")[sort_index].innerHTML;
var swap = 0;
switch (sort_index)
{
// Alphabetic sort
case 0:
case 1:
case 2:
if (b.toLowerCase() < a.toLowerCase())
swap = 1;
break;
// Numeric sort
case 3:
if (b - a < 0)
swap = 1;
break;
}
if (swap == 1)
{
$(".row-item").eq(i - 1).after(table.rows[j]);
$(".row-item").eq(j - 1).after(table.rows[i]);
}
}
}
});
}
});
Edit
It appears the real problem was related to closures inside loops. When a header is clicked, only the last table-row swap is actually updated in DOM, and as such multiple clicks are needed to sort the table properly.
I will post my own solution to this, to clarify the real problem.
Description
Table rows must be swapped at arbitrary positions in the table, i.e. row i and row j must change positions, where i and j are not necessarily adjacent. See current implementation below. Table rows should be sorted by column; a column is specified by sort_index
which is generated by pressing one of the table headers.
The problem with the implementation is that the table is sorted incrementally by each click of a header, while it should be sorted by a single click.
var table_index = {
watched: 0,
title: 1,
director: 2,
year: 3
};
var sort_index = 0;
$(document).ready(function()
{
var table_headers = document.getElementById("header-item").children;
for (var k = 0; k < table_headers.length; k++)
{
$("#" + table_headers[k].id).bind("click", function(e)
{
sort_index = table_index[e.target.id];
var table = document.getElementById("film-list");
for (var i = 1; i < table.rows.length - 1; i++)
{
var a = table.rows[i].getElementsByTagName("td")[sort_index].innerHTML;
for (var j = i + 1; j < table.rows.length; j++)
{
var b = table.rows[j].getElementsByTagName("td")[sort_index].innerHTML;
var swap = 0;
switch (sort_index)
{
// Alphabetic sort
case 0:
case 1:
case 2:
if (b.toLowerCase() < a.toLowerCase())
swap = 1;
break;
// Numeric sort
case 3:
if (b - a < 0)
swap = 1;
break;
}
if (swap == 1)
{
$(".row-item").eq(i - 1).after(table.rows[j]);
$(".row-item").eq(j - 1).after(table.rows[i]);
}
}
}
});
}
});
Edit
It appears the real problem was related to closures inside loops. When a header is clicked, only the last table-row swap is actually updated in DOM, and as such multiple clicks are needed to sort the table properly.
I will post my own solution to this, to clarify the real problem.
Share Improve this question edited Jan 12, 2019 at 20:29 asked Jan 8, 2019 at 14:47 user4619150user4619150 2- 1 I think that binding a data model to the table would work better. That way you could modify the underlying data and then re-render the table with the data. – Mr. Polywhirl Commented Jan 8, 2019 at 14:50
- 1 It's a bit curious that you're using jQuery, but also going direct to the DOM for most of the things jQuery simplifies for you... It may be worthwhile studying jQuery in depth if you want to use it, or studying the DOM in depth if you don't want to use a library. – T.J. Crowder Commented Jan 8, 2019 at 15:01
3 Answers
Reset to default 3I agree with Mr Polywhirl that doing this in the DOM itself probably isn't ideal (though it's entirely possible, see below). Modern web development leans toward using model/view/controller-style architectures (of various types) where your model (your actual data) is held separately from the view of it (the DOM), and the controller (the browser, DOM, and your code operating together), which takes actions upon your model (which are then reflected in the view). There are many popular MVC-style frameworks, probably the most significant as I write this (it changes over time) are React, Vue.js, and Angular. I've included a React example below.
But again, you can do this directly on the DOM. I see you're using jQuery, so I've used it below — See inline ments.
// Hook `click` on the table header, but only call our callback if
// that click passes through a `th`
$(".sortable thead").on("click", "th", function() {
// Which column is this?
var index = $(this).index();
// Get the tbody
var tbody = $(this).closest("table").find("tbody");
// Disconnect the rows and get them as an array
var rows = tbody.children().detach().get();
// Sort it
rows.sort(function(left, right) {
// Get the text of the relevant td from left and right
var $left = $(left).children().eq(index);
var $right = $(right).children().eq(index);
return $left.text().localeCompare($right.text());
});
// Put them back in the tbody
tbody.append(rows);
});
td, th {
padding: 4px;
}
th {
cursor: pointer;
}
table {
border-collapse: collapse;
}
table, td, th {
border: 1px solid #ddd;
}
To sort the rows alphabetically by a column's contents, click its header.
<table class="sortable">
<thead>
<th>English</th>
<th>Spanish</th>
<th>Italian</th>
</thead>
<tbody>
<tr>
<td>One</td>
<td>Uno</td>
<td>Uno</td>
</tr>
<tr>
<td>Two</td>
<td>Dos</td>
<td>Due</td>
</tr>
<tr>
<td>Three</td>
<td>Tres</td>
<td>Tre</td>
</tr>
<tr>
<td>Four</td>
<td>Cuatro</td>
<td>Quattro</td>
</tr>
</tbody>
</table>
<script src="https://cdnjs.cloudflare./ajax/libs/jquery/3.3.1/jquery.min.js"></script>
It can be a bit shorter, but I wanted to be clear rather than hyper-concise.
Notice that this removes the rows, sorts them, and puts them back, rather than causing all kinds of in-place DOM modifications.
Here's the React example:
// A React "ponent" for the table
class MyTable extends React.Component {
// Initializes the ponent
constructor(props) {
super(props);
this.state = {
// Start out unsorted, copying the array (but reusing the entries, as `row`
// properties -- we do that so we can use their original index as a key)
sorted: props.data.map((row, index) => ({index, row})),
sortKey: null
};
}
// Sort the view
sort(by) {
// Update state...
this.setState(({sorted, sortKey}) => {
if (sortKey === by) {
// ...no update needed, already sorting this way
return;
}
// Copy the array, then sort it (never change state in place)
sorted = sorted.slice();
sorted.sort((left, right) => left.row[by].localeCompare(right.row[by]));
// Return the state updates
return {sorted, sortKey: by};
});
}
// Render the ponent per current state
render() {
const {sorted} = this.state;
const {headers} = this.props;
return (
<table className="sortable">
<thead>
{headers.map(({title, lang}) => <th key={lang} onClick={() => this.sort(lang)}>{title}</th>)}
</thead>
<tbody>
{sorted.map(({row, index}) =>
<tr key={index}>
{headers.map(({lang}) => <td key={lang}>{row[lang]}</td>)}
</tr>
)}
</tbody>
</table>
);
}
}
// Mount the ponent
ReactDOM.render(
<MyTable
headers={[
{title: "English", lang: "en"},
{title: "Spanish", lang: "es"},
{title: "Italian", lang: "it"}
]}
data={[
{en: "One", es: "Uno", it: "Uno"},
{en: "Two", es: "Dos", it: "Due"},
{en: "Three", es: "Tres", it: "Tre"},
{en: "Four", es: "Cuatro", it: "Quattro"}
]}
/>,
document.getElementById("root")
);
td, th {
padding: 4px;
}
th {
cursor: pointer;
}
table {
border-collapse: collapse;
}
table, td, th {
border: 1px solid #ddd;
}
To sort the rows alphabetically by a column's contents, click its header.
<div id="root"></div>
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
That uses various ES2015+ features, such as destructuring, arrow functions, and shorthand properties; and also uses JSX syntax (which isn't a JavaScript feature; it's handled by Babel in the Snippet).
You can either sort the HTML in-place or you can bind data to the table and re-render it whenever you need to sort.
I used T.J. Crowder's code for in-place sorting, but turned it into a jQuery plugin and added a bindable table. You can see both examples below.
(function($) {
$.fn.sortable = function() {
this.find('thead').on('click', 'th', function(e) {
var columnIndex = $(this).index();
var $tbody = $(this).closest('table').find('tbody');
var rows = $tbody.children().detach().get();
rows.sort(function(left, right) {
var $left = $(left).children().eq(columnIndex);
var $right = $(right).children().eq(columnIndex);
return $left.text().localeCompare($right.text());
});
$tbody.append(rows);
});
return this;
};
$.fn.renderTable = function(data) {
var fields = Object.keys(data[0]);
return this.renderTableHeaders(fields).renderTableRows(fields, data);
};
$.fn.renderTableHeaders = function(fields) {
return this.append($.renderTableHeaders(fields));
}
$.fn.renderTableRows = function(fields, data) {
return this.append($.renderTableRows(fields, data));
};
$.tableFromJson = function(data) {
return $('<table>').renderTable(data);
};
$.renderTableHeaders = function(fields) {
return $('<thead>').append($('<tr>').append(fields
.map(field => $('<th>').text(field))));
};
$.renderTableRows = function(fields, data) {
return $('<tbody>').append(data
.map((rec, row) => $('<tr>').append(fields
.map((field, col) => $('<td>').text(rec[field])))));
};
$.bindableTable = function(data, sortable) {
var $table = $.tableFromJson(data).addClass('bindable');
if (sortable) {
$table.dataRef = data;
$table.addClass('sortable').find('thead').on('click', 'th', function(e) {
var dataIndex = $(this).text();
$table.dataRef.sort(function (a, b) {
var left = new String(a[dataIndex]);
var right = new String(b[dataIndex]);
return left.localeCompare(right);
});
var fields = Object.keys($table.dataRef[0]);
$table.find('tbody').replaceWith($.renderTableRows(fields, $table.dataRef));
});
}
return $table;
};
})(jQuery);
var jsonData = [
{ "id": 1, "name": "John", "age": 24, "make": "Chevrolet", "model": "Silverado", "year": 2016 },
{ "id": 2, "name": "Jack", "age": 36, "make": "Toyota", "model": "Corolla", "year": 2018 },
{ "id": 3, "name": "Jill", "age": 29, "make": "Ford", "model": "Escape", "year": 2015 }
];
$('body').append($('<h1>').text('HTML sort'));
$.tableFromJson(jsonData).addClass('stylized sortable').sortable().appendTo('body');
$('body').append($('<h1>').text('Databinding sort'));
$.bindableTable(jsonData, true).addClass('stylized').appendTo('body');
body {
padding: 0.25em !important;
}
h1 {
font-weight: bold !important;
margin-top: 0.75em !important;
margin-bottom: 0.33em !important;
}
table.stylized {
font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
font-size: 12px;
text-align: left;
border-collapse: collapse;
margin: 4px;
width: 600px;
}
table.stylized thead th {
text-transform: capitalize;
font-size: 13px;
color: #039;
background: #b9c9fe;
padding: 6px;
cursor: pointer;
}
table.stylized tbody tr:nth-child(odd) {
background: #f2f5ff;
}
table.stylized tbody tr:nth-child(even) {
background: #e8edff;
}
table.stylized tbody td {
border-top: 1px solid #fff;
color: #669;
padding: 6px;
}
table.stylized tbody tr:hover td {
background: #d0dafd;
}
<link href="https://cdnjs.cloudflare./ajax/libs/meyer-reset/2.0/reset.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare./ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Description
As stated, the original problem was swapping of rows. It appears the real problem was related to closures inside loops. When a header is clicked, only the last table-row swap is actually updated in DOM, and as such multiple clicks are needed to sort the table properly.
Solution
One possible solution is to use the built-in sort function, as shown below.
var table_index = {
watched: 0,
title: 1,
director: 2,
year: 3
};
var sort_index = 0;
var tbody = $("tbody").children().get();
$(document).ready(function()
{
var table_headers = $("thead").children();
for (var k = 0; k < table_headers.length; k++)
{
$("#" + table_headers[k].id).bind("click", function(e)
{
sort_index = table_index[e.target.id];
switch (sort_index)
{
// Alphabetic sort
case 0:
case 1:
case 2:
tbody.sort(function(a, b) {
var l = $(a).children().eq(sort_index).text();
var r = $(b).children().eq(sort_index).text();
if (r.toLowerCase() < l.toLowerCase())
return 1;
else if (r.toLowerCase() > l.toLowerCase())
return -1;
else
return 0;
});
break;
// Numeric sort
case 3:
tbody.sort(function(a, b) {
var l = $(a).children().eq(sort_index).text();
var r = $(b).children().eq(sort_index).text();
return l - r;
});
break;
}
$("tbody").children().detach();
$("tbody").append(tbody);
});
}
});