Sortable data tables using pure JavaScript

Murat Dogan
Oct 31 · 6 min read

It’s been a while for writing technical articles, that’s why I decided to write a new one but with one small difference. I decided to write this article in English. Here we go:

Credit: https://unsplash.com/@_imkiran

There was a feature request in my current company, product team requested a table component which should order columns in ascending or descending way when clicking the column’s title. At the end of this post, you’ll see the working POC. There may be so many things to improve in the aspect of code quality but do not forget, this is just a POC. I’m looking forward to your responses to the code.


I have a table which has fixed headings. I have an array of objects including just four elements for the sake of simplicity. This table should be sorted by some properties of these objects, not all of them. I have to build this table without using any of the view libraries, just plain-old JavaScript!

I usually pay attention to write my functions by adopting the Single Responsibility Principle. This way provides so much convenience when both writing and debugging.

<table id="squadTable" class="table table-dark table-striped">
<thead>
<tr>
<th data-filter-value="no" class="active">No</th>
<th>Name</th>
<th>Position</th>
<th data-filter-value="age">Age</th>
<th data-filter-value="match">Match</th>
<th data-filter-value="time">Min</th>
<th data-filter-value="goal">Goal</th>
<th data-filter-value="assist">Ast</th>
<th data-filter-value="yellowCard">YC</th>
<th data-filter-value="doubleYellowCard">DYC</th>
<th data-filter-value="redCard">RC</th>
</tr>
</thead>
<tbody>
</tbody>
</table>

I shared the Codepen link at the end of the post. I implemented the Bootstrap for a better look and feel. This is the purpose of some generic classes in the code above.

I have also highlighted the sortable columns by making them bold in the code. As I mentioned earlier this is a static POC so I’ve statically written data-filter-values. You can change them to template literals and generate using an array etc.

th {
&[data-filter-value] {
cursor: pointer;
} &.active {
color: red;
}
}

I do not have any CSSish concerns just because I’m using Bootstrap. I just want to differentiate the sortable and active columns. I added the Scss codes above.

Here is my data array of objects:

const data = [{
"id": "1233213",
"no": 17,
"fullName": "Murat Doğan",
"position": "CMD",
"age": 29,
"match": 12,
"time": 1080,
"goal": 12,
"assist": 4,
"yellowCard": 3,
"doubleYellowCard": 0,
"redCard": 2
}, {
"id": "41233213",
"no": 58,
"fullName": "Volkan Demirel",
"position": "GK",
"age": 28,
"match": 34,
"time": 3400,
"goal": 1,
"assist": 17,
"yellowCard": 8,
"doubleYellowCard": 3,
"redCard": 5
}, {
"id": "51233213",
"no": 18,
"fullName": "Soldado",
"position": "ST",
"age": 33,
"match": 5,
"time": 120,
"goal": 1,
"assist": 2,
"yellowCard": 5,
"doubleYellowCard": 1,
"redCard": 4
}, {
"id": "61233213",
"no": 27,
"fullName": "Hasan Ali Kaldırım",
"position": "DL",
"age": 18,
"match": 2,
"time": 12,
"goal": 0,
"assist": 1,
"yellowCard": 3,
"doubleYellowCard": 4,
"redCard": 5
}];

I don’t use id properties but it’s good to have unique keys for any purpose in the future.

These are the global variables for sorting:

let currentFilter = "no",
prevFilter = "",
orderAsc = true;
  • The default sorting value of the table will be the number of the player(no). I set it as the current filter.
  • Prev filter value keeps the previously selected filter. It’s important to keep that data to handle click events applied to the same column. Converting it from ascending to descending or vice versa.
  • order ascending value keeps the current ordering type. It’s going to be changed regarding click actions.
const toggleOrder = () => {
if (currentFilter === prevFilter) {
orderAsc = !orderAsc; // Get same value and assign its opposite.
} else {
orderAsc = true;
}
}

As I mentioned before, If the user clicks to the same column name over and over it changes the direction of the ordering. I need an else situation here because if I order a column as descending other columns need to be ordered ascending. It’s kind of resetting order :)

const sortTable = (array, sortKey) => {
return array.sort((a, b) => {
let x = a[sortKey],
y = b[sortKey]; return orderAsc ? x - y : y - x; // ternary operator
});
}

This is the function taking two arguments as an array and a sorting variable. I’m using Array.sort method here to catch the difference between the previous and next variables. If you want to compare the string values you can use greater than or less than signs. If you want to order by ascending it uses x-y or y-x for the opposite situation.

Here it comes to rendering table :

const renderTable = tableData => {
return (`${tableData.map(item => {
return (
`<tr>
<td>${item.no}</td>
<td>${item.fullName}</td>
<td>${item.position}</td>
<td>${item.age}</td>
<td>${item.match}</td>
<td>${item.time}</td>
<td>${item.goal}</td>
<td>${item.assist}</td>
<td>${item.yellowCard}</td>
<td>${item.doubleYellowCard}</td>
<td>${item.redCard}</td>
</tr>`
)
}).join('')}`);
}

It’s the only purpose of this function that rendering the tr elements regarding given data. It loops using the map through the array and returns string output for each of them. If you are curious about the join method at the end of the chain, let me explain! Map function returns a new array and that join method helps us to join these array items by adding an empty string at the end of each item. source: Mdn

const appendTable = (table, destination) => {
document.querySelector(destination).innerHTML = table;
}

This function’s only purpose is appending the giving element to the given destination. The naming of this function might be more generic but as I mentioned before this is just a POC. You should rename it to append etc.

const handleSortClick = () => {
const filters = document.querySelectorAll('#squadTable th');
Array.prototype.forEach.call(filters, filter => {
filter.addEventListener("click", () => {
if (!filter.dataset.filterValue) return false;
Array.prototype.forEach.call(filters, filter => {
filter.classList.remove('active');
});
filter.classList.add('active');
currentFilter = filter.dataset.filterValue;
toggleOrder();
init();
});
})
}

It’s better to keep this section more detailed. I hardcoded the selector of the columns’ heading but you can pass selector as an argument. It’s not that hard.

I’m querying all the th elements and looping using a forEach. If you’re curious about why I didn’t use Array.forEach, let me explain: filters variable is a list but it’s just a node list. If you’re dealing with node lists you can’t use every method of Array. It provides both browser compatibility and access to the forEach.

I’m checking whether the header has a sorting property or not. If not, I return false at the very beginning of the function and do not execute the whole function.

I want to mention this pleasant feature: dataset. If you want to access the whole data attributes of a dom element, you can list all of them by using the dataset instead of using getAttribute(…). You should beware of the usage. data attributes are kebap-case but you need to access them by using camelCase.

I’m doing some class job and triggering toggle function. I’m triggering init function, as well.

const init = () => {
let newTableData = sortTable(data, currentFilter),
tableOutput = renderTable(newTableData);appendTable(tableOutput, '#squadTable tbody');
prevFilter = currentFilter;
}

I’m doing all the short works here. Let’s have a look at what’s going on:

  • I’m reformatting my player data using the current filter parameter. It helps me by making sorting easily when I change the current filter inside other functions.
  • Creating string Html output for the table assigning to the tableOutput.
  • I’m appending this output to where I declare.
  • I can assign the current filter as the previous filter at the end of the whole process finishes.
init();
handleSortClick();

I call init function inside other functions so I do not call handleSortClick inside the init function. This prevents rebinding the events to the selectors. You may share better ways to improve this code.

It takes only ~70 lines of code when creating the table. If we keep the data objects separated. It depends on the template but I think it’s quite short and easy to implement.

Thanks for reading this article until this line. I’m looking forward to your responses to make this code even better.

Learn the web's most important programming language.

Murat Dogan

Written by

JavaScript in Plain English
Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade