1. Inserting content with HTML
The <table>
tag is the semantic HTML tag used to display data on a web page. We will put ours <table>
tag inside a div table-container which will allow us to include some responsive styles in the CSS.
<div class="table-container"> <table class="data-table"> </table> </div>
Our table will contain the table header, thead
and contents of the table, tbody
tag. In the header of our table, we will include buttons in each th
cell that will be used to manage the sorting functionality. The cells for the table content will be added with JavaScript using the data from our JSON response.
<div class="table-container"> <table class="data-table"> <thead> <tr> <th><button id="name">Name</button></th> <th><button id="type">Type</button></th> <th><button id="hp">HP</button></th> <th><button id="attack">Attack</button></th> <th><button id="defense">Defense</button></th> <th><button id="spAttack">Sp. Attack</button></th> <th><button id="spDefense">Sp. Defense</button></th> <th><button id="speed">Speed</button></th> <th><button id="total">Total</button></th> </tr> </thead> <tbody id="table-content"></tbody> </table> </div>
2. Making a responsive table with CSS
One of the most common problems in using HTML tables is the lack of responsiveness. The table may have a cell overlap problem or go beyond the boundaries of the entire page.
We can get rid of these problems by placing the table in a full page width table container with an overflow scroll property. In this way, the table is always as wide as the entire page and there is no need to shrink the cells thanks to the sliding overflow. We will also include minimum width properties in table cells to avoid text wrapping.
This is the CSS needed to make our table scrollable:
.table-container { width: 100%; overflow: scroll; } table { width: 100%; }
We can then add the rest of our style:
.table-container { margin: auto; max-width: 1200px; min-height: 100vh; overflow: scroll; width: 100%; } table { border-collapse: collapse; width: 100%; } thead tr { border-bottom: 1px solid #ddd; border-top: 1px solid #ddd; height: 1px; } th { font-weight: bold; height: inherit; padding: 0; } th:not(:first-of-type) { border-left: 1px solid #ddd; } th button { background-color: #eee; border: none; cursor: pointer; display: block; font: inherit; height: 100%; margin: 0; min-width: max-content; padding: 0.5rem 1rem; position: relative; text-align: left; width: 100%; } tbody tr { border-bottom: 1px solid #ddd; } td { padding: 0.5rem 1rem; text-align: left; }
3. Inserting JSON data into an HTML table
For this example, we will work with a mock parsed JSON response. Here’s what our data looks like:
const response = { "pokedata": [ { "name": "Bulbasaur", "type": "Grass", "hp": 45, "attack": 49, "defense": 49, "spAttack": 65, "spDefense": 65, "speed": 45, "total": 318 }, ... ] }
We will put the data inside ours <tbody id="table-content">
tag to target that element in JavaScript;
const tableContent = document.getElementById("table-content")
The content of the row will be based on each object within ours response.pokedata
. Let’s define a function to create a new row based on the object data:
const createRow = (obj) => { const row = document.createElement("tr"); const objKeys = Object.keys(obj); objKeys.map((key) => { const cell = document.createElement("td"); cell.setAttribute("data-attr", key); cell.innerHTML = obj[key]; row.appendChild(cell); }); return row; };
In this function we use the Object.keys()
method to get the keys of the object as an array. Here’s what the return value looks like:
Once we have the object’s key array, we loop through each key using the .map()
method. The map method executes the function we passed to it on each element in the array.
In this map function, we create a new cell for each element in the array and set the innerHTML cell as the corresponding object key value.
Finally, we add the cell we created to the row defined at the start of the function using the .appendChild()
method.
Now that we have our line creation function, we will define a function to iterate over the response.pokedata
array and add each new row to ours tableContent
element.
const getTableContent = (data) => { data.map((obj) => { const row = createRow(obj); tableContent.appendChild(row); }); };
We will pass ours getTableContent
function in an event listener so that content is added to the table once the page is loaded.
window.addEventListener("load", () => { getTableContent(response.pokedata); });
4. Sorting data with JavaScript
Now that we’ve created our table, let’s add sorting functionality. In our HTML, we included the buttons in the header cells that had the object keys as the ID. Let’s now target those buttons:
const tableButtons = document.querySelectorAll("th button");
We want to manage the sorting of data based on the button clicked and also include a function that toggles the sort direction (ascending or descending) when the button is clicked.
We can use the .sort()
method to manage the sorting of data in our response.pokedata
Vector. The sorting method accepts a function that compares two different parameters and sorts them based on the function’s response:
compareFunction(a, b) return value
|
sorting |
---|---|
> 0 |
to sort a after b
|
<0 | to sort a Before b |
=== 0 | keep the original order of a And b
|
Source: MDN
Another thing to note about the sorting method is that it changes the original array it operates on. This means it changes the value of our original array.
We can avoid mutating our original array by using spread syntax […]
Now we can create our sorting function. Here’s what the logic for our sort function will look like:
- Clears the content in the tableContent element
- Sorts the data according to the selected parameter and direction
- Add the sorted data to our tableContent using the file
getTableContent
function
const sortData = (data, param, direction = "asc") => { tableContent.innerHTML = ''; const sortedData = direction == "asc" ? [...data].sort(function (a, b) { if (a[param] < b[param]) { return -1; } if (a[param] > b[param]) { return 1; } return 0; }) : [...data].sort(function (a, b) { if (b[param] < a[param]) { return -1; } if (b[param] > a[param]) { return 1; } return 0; }); getTableContent(sortedData); };
Our sort function accepts three parameters:
-
data
: the array to be ordered -
param
: the value used to sort the array -
direction
: sorts the array in ascending or descending order. The default value of the parameter is set to “asc”.
We clear the content in our tableContent element by setting innerHTML to an empty string. We therefore use the .sort()
method e direction
parameter to determine how the data should be sorted. We reverse the comparison function to sort in descending order. Using the compare function in this way allows us to sort the data regardless of the type value (string, integer, float etc.)
Finally, we pass the sortedData
as a new value in the content of our table.
We will now pass our sort function into a click event listener for our table buttons and also handle the toggle of the sort direction.
window.addEventListener("load", () => { getTableContent(response.pokedata); [...tableButtons].map((button) => { button.addEventListener("click", (e) => { if (e.target.getAttribute("data-dir") == "desc") { sortData(response.pokedata, e.target.id, "desc"); e.target.setAttribute("data-dir", "asc"); } else { sortData(response.pokedata, e.target.id, "asc"); e.target.setAttribute("data-dir", "desc"); } }); }); });
In this function, we manage the switching by setting a data-dir
attribute on our buttons to determine the sort direction. We will also update our CSS to display an icon next to the button depending on the sort direction:
th button::after { position: absolute; right: 0.5rem; } th button[data-dir="asc"]::after { content: url("data:image/svg+xml,%3Csvg xmlns="https://www.w3.org/2000/svg" width="8" height="8"%3E%3Cpolygon points="0, 0 8,0 4,8 8" fill="%23818688"/%3E%3C/svg%3E"); } th button[data-dir="desc"]::after { content: url("data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="8" height="8"%3E%3Cpolygon points="4 0,8 8,0 8" fill="%23818688"/%3E%3C/svg%3E"); }
We don’t want icons to appear on all previously clicked buttons, so we’ll define a resetButtons
function that removes the data-dir attribute on any button other than the button clicked.
const resetButtons = (event) => { [...tableButtons].map((button) => { if (button !== event.target) { button.removeAttribute("data-dir"); } }); };
We will pass that function to our button event listener to revert to the previous buttons each time a new button is clicked
window.addEventListener("load", () => { getTableContent(response.pokedata); [...tableButtons].map((button) => { button.addEventListener("click", (e) => { resetButtons(e); if (e.target.getAttribute("data-dir") == "desc") { sortData(response.pokedata, e.target.id, "desc"); e.target.setAttribute("data-dir", "asc"); } else { sortData(response.pokedata, e.target.id, "asc"); e.target.setAttribute("data-dir", "desc"); } }); }); });
Conclusion
And with that, we’re done! We created a sortable table using vanilla JavaScript only!