skip to Main Content

I stumbled upon Nick Grealy’s answer to a question on here about sorting tables (https://stackoverflow.com/a/49041392), with jedwards fix for people having rows inside a tbody (https://stackoverflow.com/a/53880407). It was a nice piece of code, which worked perfectly for what I wanted it for.

I am just having a hard time implementing sorting symbols in the th headers of the table for when the table is sorted ascending or descending for a specific column. I came up with this which works somewhat okay:

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));
document.querySelectorAll('th').forEach(th => th.addEventListener('click', ((e) => { //own code: added "e" to use further down
    const table = th.closest('table');
    const tbody = table.querySelector('tbody');
    let buttons = document.getElementsByTagName("button"); //getting all button tags
    const arrayButtons = ['0', '1', '2']; //own code: for use further down, as I have 3 buttons
    Array.from(tbody.querySelectorAll('tr'))
        .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => tbody.appendChild(tr));
    arrayButtons.forEach(e => buttons[e].setAttribute("data-dir", "")); //own code: reset all data-dir fields in buttons
    if (this.asc) { //own code: if ascending, fill in asc in data-dir field in button, else data-dir desc gets filled in.
        e.target.setAttribute("data-dir", "asc");
    } else {
        e.target.setAttribute("data-dir", "desc");
    }
})));
table, th, td {
  border: 1px solid black;
  border-collapse: collapse;
}
th button {
  background-color: transparent;
  border: none;
  cursor: pointer;
  font: inherit;
  color:inherit;
  width: 100%;
}
th button[data-dir="asc"]::after {
  content: " " url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 13' xml:space='preserve' width='13' height='13' transform='scale(1 -1)'%3E%3Cpath d='M2.039 5.171c.114.114.25.172.406.172h8.107a.55.55 0 0 0 .406-.172c.114-.114.172-.25.172-.406s-.057-.292-.172-.406L6.906.302C6.792.187 6.658.13 6.5.13s-.292.057-.406.172L2.039 4.355a.557.557 0 0 0-.172.406.541.541 0 0 0 .172.408zM6.5 12.473a.163.163 0 0 1-.127-.055L2.319 8.364a.159.159 0 0 1-.055-.127c0-.042.01-.081.055-.127s.084-.055.127-.055h8.107c.042 0 .081.01.127.055s.055.084.055.127a.163.163 0 0 1-.055.127l-4.052 4.054c-.047.045-.084.055-.127.055m0 .396a.55.55 0 0 0 .406-.172l4.054-4.054a.557.557 0 0 0 .172-.406.549.549 0 0 0-.172-.406.557.557 0 0 0-.406-.172H2.447a.549.549 0 0 0-.406.172.557.557 0 0 0-.172.406.55.55 0 0 0 .172.406l4.054 4.053c.114.114.25.172.406.172z' fill='%23000'/%3E%3C/svg%3E");
}
th button[data-dir="desc"]::after {
  content: " " url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 13' xml:space='preserve' width='13' height='13'%3E%3Cpath d='M2.039 5.171c.114.114.25.172.406.172h8.107a.55.55 0 0 0 .406-.172c.114-.114.172-.25.172-.406s-.057-.292-.172-.406L6.906.302C6.792.187 6.658.13 6.5.13s-.292.057-.406.172L2.039 4.355a.557.557 0 0 0-.172.406.541.541 0 0 0 .172.408zM6.5 12.473a.163.163 0 0 1-.127-.055L2.319 8.364a.159.159 0 0 1-.055-.127c0-.042.01-.081.055-.127s.084-.055.127-.055h8.107c.042 0 .081.01.127.055s.055.084.055.127a.163.163 0 0 1-.055.127l-4.052 4.054c-.047.045-.084.055-.127.055m0 .396a.55.55 0 0 0 .406-.172l4.054-4.054a.557.557 0 0 0 .172-.406.549.549 0 0 0-.172-.406.557.557 0 0 0-.406-.172H2.447a.549.549 0 0 0-.406.172.557.557 0 0 0-.172.406.55.55 0 0 0 .172.406l4.054 4.053c.114.114.25.172.406.172z' fill='%23000'/%3E%3C/svg%3E");
}
<table width='100%'>
<thead>
<tr>
<th width='20%'>Image</th>
<th width='20%'><button data-dir=''>Number</button></th>
<th width='40%'><button data-dir=''>Name</button></th>
<th width='20%'><button data-dir=''>Postal code</button></th>
</tr>
</thead>
<tbody>
<tr>
<td>IMG1</td>
<td>xxx</td>
<td>John Johnson</td>
<td>56430</td>
</tr>
<tr>
<td>IMG2</td>
<td>yyy</td>
<td>Sally Johnson</td>
<td>56430</td>
</tr>
</tbody>
</table>

There are some problems with the code that I can’t seem to solve, and which I am asking for help to fix:

  1. When I click the th-header of the image-column, it sets the data-dir of the th and removes the data-dir from all the other columns and basically starts sorting the image-column. The image-column does not have a button and is not supposed to be able to trigger the sorting function or the data-dir function. Any way to fix this?
  2. When a table is already sorted ascending, for example the Name-column, when clicking the th-header for the name-column, it sorts it ascending first (even though it already is) and then on second-click descending, instead of descending right away. Any way to fix this?
  3. Is the code optimized as it is, especially the parts I have added with the let buttons, const arrayButtons and the if else for setAttribute of data-dir? Could it be more optimized?

I am trying to learn, so any feedback would be appreciated. 🙂

Thank you in advance!

2

Answers


  1. import React, { useState } from "react";
    import Column from "./Column";
    import Row from "./Row";
    import getSortedData from "./getSortedData";
    
    import "./styles.css";
    
    const data = [
      { name: "Manu", score: 98, age: 21 },
      { name: "Fran", score: 70, age: 43 },
      { name: "Jose", score: 22, age: 22 },
      { name: "Ana", score: 23, age: 99 },
      { name: "Luis", score: 33, age: 14 },
      { name: "Simon", score: 47, age: 54 },
      { name: "Xavier", score: 44, age: 35 }
    ];
    
    export default function App() {
      const [sortedBy, setSortedBy] = useState(null);
      const [direction, setDirection] = useState("UP");
    
      const onSortBy = name => {
        if (sortedBy !== name) {
          setSortedBy(name);
        } else if (direction === "UP") {
          setDirection("DOWN");
        } else {
          setSortedBy(null);
          setDirection("UP");
        }
      };
    
      const sortedData = getSortedData(data, sortedBy, direction);
    
      return (
        <div className="App">
          <table>
            <tr>
              <Column
                name="name"
                direction={direction}
                sortedBy={sortedBy}
                onSortBy={onSortBy}
              />
              <Column
                name="score"
                direction={direction}
                sortedBy={sortedBy}
                onSortBy={onSortBy}
              />
              <Column
                name="age"
                direction={direction}
                sortedBy={sortedBy}
                onSortBy={onSortBy}
              />
            </tr>
            {sortedData.map(player => (
              <Row player={player} />
            ))}
          </table>
        </div>
      );
    }
    
    Login or Signup to reply.
  2. Here is a working version, there were a couple of problems.
    It was sorting even the Image column because you’ve used document.querySelectorAll('th') and added a click listener on all those elements instead of doing that for the <button> elements.

    Your version doesn’t remember the previous sorting direction because you’ve used this.asc which is a global variable not something associated with the specific column.
    To fix that, I’ve used button.dataset.prevDir to keep track of the previous direction without always showing the arrows.

    As for the optimisation. No arrayButtons is not the way to go.
    In my example I’ve used buttons which is a list of all the buttons and instead of setAttribute, I’ve used dataset.

    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));
    const buttons = document.querySelectorAll('button');
    buttons.forEach(button => button.addEventListener('click', ((e) => {
        const th = button.parentNode;
        const table = th.closest('table');
        const tbody = table.querySelector('tbody');
        button.dataset.prevDir = button.dataset.prevDir === 'asc' ? 'desc' : 'asc';
        buttons.forEach((btn) => (btn.dataset.dir = ''));
        Array.from(tbody.querySelectorAll('tr'))
            .sort(comparer(Array.from(th.parentNode.children).indexOf(th), button.dataset.prevDir === 'asc'))
            .forEach(tr => tbody.appendChild(tr));
        button.dataset.dir = button.dataset.prevDir;
    })));
    table, th, td {
      border: 1px solid black;
      border-collapse: collapse;
    }
    th button {
      background-color: transparent;
      border: none;
      cursor: pointer;
      font: inherit;
      color:inherit;
      width: 100%;
    }
    th button[data-dir="asc"]::after {
      content: " " url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 13' xml:space='preserve' width='13' height='13' transform='scale(1 -1)'%3E%3Cpath d='M2.039 5.171c.114.114.25.172.406.172h8.107a.55.55 0 0 0 .406-.172c.114-.114.172-.25.172-.406s-.057-.292-.172-.406L6.906.302C6.792.187 6.658.13 6.5.13s-.292.057-.406.172L2.039 4.355a.557.557 0 0 0-.172.406.541.541 0 0 0 .172.408zM6.5 12.473a.163.163 0 0 1-.127-.055L2.319 8.364a.159.159 0 0 1-.055-.127c0-.042.01-.081.055-.127s.084-.055.127-.055h8.107c.042 0 .081.01.127.055s.055.084.055.127a.163.163 0 0 1-.055.127l-4.052 4.054c-.047.045-.084.055-.127.055m0 .396a.55.55 0 0 0 .406-.172l4.054-4.054a.557.557 0 0 0 .172-.406.549.549 0 0 0-.172-.406.557.557 0 0 0-.406-.172H2.447a.549.549 0 0 0-.406.172.557.557 0 0 0-.172.406.55.55 0 0 0 .172.406l4.054 4.053c.114.114.25.172.406.172z' fill='%23000'/%3E%3C/svg%3E");
    }
    th button[data-dir="desc"]::after {
      content: " " url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 13' xml:space='preserve' width='13' height='13'%3E%3Cpath d='M2.039 5.171c.114.114.25.172.406.172h8.107a.55.55 0 0 0 .406-.172c.114-.114.172-.25.172-.406s-.057-.292-.172-.406L6.906.302C6.792.187 6.658.13 6.5.13s-.292.057-.406.172L2.039 4.355a.557.557 0 0 0-.172.406.541.541 0 0 0 .172.408zM6.5 12.473a.163.163 0 0 1-.127-.055L2.319 8.364a.159.159 0 0 1-.055-.127c0-.042.01-.081.055-.127s.084-.055.127-.055h8.107c.042 0 .081.01.127.055s.055.084.055.127a.163.163 0 0 1-.055.127l-4.052 4.054c-.047.045-.084.055-.127.055m0 .396a.55.55 0 0 0 .406-.172l4.054-4.054a.557.557 0 0 0 .172-.406.549.549 0 0 0-.172-.406.557.557 0 0 0-.406-.172H2.447a.549.549 0 0 0-.406.172.557.557 0 0 0-.172.406.55.55 0 0 0 .172.406l4.054 4.053c.114.114.25.172.406.172z' fill='%23000'/%3E%3C/svg%3E");
    }
    <table width='100%'>
    <thead>
    <tr>
    <th width='20%'>Image</th>
    <th width='20%'><button data-dir=''>Number</button></th>
    <th width='40%'><button data-dir=''>Name</button></th>
    <th width='20%'><button data-dir=''>Postal code</button></th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>IMG1</td>
    <td>xxx</td>
    <td>John Johnson</td>
    <td>56430</td>
    </tr>
    <tr>
    <td>IMG2</td>
    <td>yyy</td>
    <td>Sally Johnson</td>
    <td>56430</td>
    </tr>
    </tbody>
    </table>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search