2
\$\begingroup\$

I have a table which i want to sort the rows visually according to a column while the rows remain intact in the DOM structure. So nodes aren't moving in the DOM or getting replaced with eachother at all.

Table rows can be sorted by clicking at the column's header as of now, which is the simplest case. In future i hope to add further functionalities such as sorting ascending/descending or sorting dynamically whenever a <td> element which belongs to the designated column gets updated. In fact i plan to turn this into a <sortable-table> custom element.

As you may notice, CSS is not my strong suit, while the code is working, the borders are a little problematic. For instance I can't tell why the row transitions has no effect on row borders or why rows do not get perfectly alligned after moving etc.

In short, i need to know if this is a proper way to approach to this task before further ado?

var rows    = [...document.querySelectorAll("tbody tr")],
    sorters = { coin  : rs => rs.sort((a,b) => a.querySelector("[headers='coin']").textContent <
                                               b.querySelector("[headers='coin']").textContent ? -1 : 1
                                     )
              , change: rs => rs.sort((a,b) => parseFloat(a.querySelector("[headers='change']").textContent) - 
                                               parseFloat(b.querySelector("[headers='change']").textContent)
                                     )
              , price : rs => rs.sort((a,b) => parseFloat(a.querySelector("[headers='price']").dataset.price) - 
                                               parseFloat(b.querySelector("[headers='price']").dataset.price)
                                     )
              },
    listen  = e => ( sorters[e.target.id](rows)
                   , requestAnimationFrame(setupTable)
                   ),
    ruler;

function setupTable(){
  var cell;
  rows.forEach((row,i) => ( cell = row.querySelector("[data-price]")
                          , cell.textContent = parseFloat(cell.dataset.price).toLocaleString("tr-TR",{minimumFractionDigits:2})
                          , row.style.transform = `translateY(${ruler[i]-row.dataset.originalTop}px)`
                          )
              );
}

document.getElementById("coin")
        .addEventListener("click", listen);
document.getElementById("change")
        .addEventListener("click", listen);
document.getElementById("price")
        .addEventListener("click", listen);

requestAnimationFrame(_ => ( ruler = rows.map(row => row.dataset.originalTop = row.getBoundingClientRect().top)
                           , setupTable()
                           ));
table {
   border-collapse: collapse;
   border: 2px solid green;
   text-align: left;
   width: 21vw;
   }

tbody tr {
  background-color: pink;
  border: 2px solid green;
  transition: transform 1s;
  transition-timing-function: ease-in-out;
  transform: translateY(0);
  }
th {
  text-align: center;
  border: 2px solid green;
  padding: 0.2em;
  cursor: pointer;
  width: 7vw;
  }   
td {
  padding: 0.2em;
  width: 7vw;
  border: 2px solid green;
  border-width: 2px 0 2px 0;
  }
tr td:nth-child(2) {
  text-align: right;
  }
tr td:nth-child(3) {
  text-align: right;
  }
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>replit</title>
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script src="script.js" defer></script>
</head>

<body>
    <table id="sorted">
        <thead>
            <tr>
                <th id="coin">Coin</th>
                <th id="change">Change</th>
                <th id="price">Price USD</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td headers="coin">BTC</td>
                <td headers="change">2%</td>
                <td headers="price" data-price="47594.70"></td>
            </tr>
            <tr>
                <td headers="coin">ETH</td>
                <td headers="change">2.11%</td>
                <td headers="price" data-price="3972.46"></td>
            </tr>
            <tr>
                <td headers="coin">BNB</td>
                <td headers="change">0.1%</td>
                <td headers="price" data-price="538.72"></td>
            </tr>
            <tr>
                <td headers="coin">USDT</td>
                <td headers="change">0%</td>
                <td headers="price" data-price="0.9997"></td>
            </tr>
            <tr>
                <td headers="coin">SOL</td>
                <td headers="change">-2%</td>
                <td headers="price" data-price="187.39"></td>
            </tr>
            <tr>
                <td headers="coin">ADA</td>
                <td headers="change">1.67%</td>
                <td headers="price" data-price="1.29"></td>
            </tr>
        </tbody>
    </table>
</body>
</html>

\$\endgroup\$
0

2 Answers 2

2
\$\begingroup\$

There are a couple technologies that may make this easier:

  • You could also use flexbox and set order and sort them based on that.
  • You could look at a CSS grid. You can see grid-row-start to order the items on the page without having to change the DOM order.

Both of these allow you to control the visual display independent of the DOM order.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for your feedback. I agree that for some applications, CSS Grid or Flexbox could be more suitable. I just wanted to implement an animated sorting functionality for those who would like to display tabular data directly with the <table> element in a more fancy way. \$\endgroup\$
    – Redu
    Commented Dec 25, 2021 at 14:27
1
\$\begingroup\$

I have been working on this for a while. Since nobody have answered i just would like to share my solution to my both problems.

1. tbody tr {transform: translateY();} doesn't effect row borders:

In order to be able to apply borders to a tr element, the table has to have {border-collapse:collapse;} in the first place. However when borders are collapsed, the table rows start sharing borders with their neigbouring rows. So when a transition is applied to a row, since the border is not solely owned by that row, stays still.

My solution was, replacing border-collapse:collapse with borders-spacing:0. Now that I can not have borders around rows, I applied borders to the necessary sides of the table, th and td elements to get the same effect.

2. Some rows do not move to the exact position that they should:

Despite all calculations being correct some of the rows wouldn't move to the exact location but shifting up or down by a tiny amount. I concluded that this is originated from sub pixel rendering. To fix it, i explicitly had to set the row height to an integer value. 27px was OK right away and it worked perfectly. I suspect the origin of the subpixel rendering should be the font size. So if we correlate the row height with the font size then perhaps we can solve the problem while maintaining the responsiveness. Let's try em units. As it turns out a tr height of 1.75, 2, 2.25, 2.5, 2.75, 3... ems do perfectly work.

var rows    = [...document.querySelectorAll("tbody tr")],
    sorters = { coin  : rs => rs.sort((a,b) => a.querySelector("[headers='coin']").textContent <
                                               b.querySelector("[headers='coin']").textContent ? -1 : 1
                                     )
              , change: rs => rs.sort((a,b) => parseFloat(a.querySelector("[headers='change']").textContent) - 
                                               parseFloat(b.querySelector("[headers='change']").textContent)
                                     )
              , price : rs => rs.sort((a,b) => parseFloat(a.querySelector("[headers='price']").dataset.price) - 
                                               parseFloat(b.querySelector("[headers='price']").dataset.price)
                                     )
              },
    listen  = e => ( sorters[e.target.id](rows)
                   , requestAnimationFrame(setupTable)
                   ),
    ruler;

function setupTable(){
  var cell;
  rows.forEach((row,i) => ( cell = row.querySelector("[data-price]")
                          , cell.textContent = parseFloat(cell.dataset.price).toLocaleString("tr-TR",{minimumFractionDigits:2})
                          , row.style.transform = `translateY(${ruler[i]-row.dataset.originalTop}px)`
                          )
              );
}

document.getElementById("coin")
        .addEventListener("click", listen);
document.getElementById("change")
        .addEventListener("click", listen);
document.getElementById("price")
        .addEventListener("click", listen);

requestAnimationFrame(_ => ( ruler = rows.map(row => row.dataset.originalTop = row.getBoundingClientRect().top)
                           , setupTable()
                           ));
table {
  border: solid green;
  border-width: 2px 2px 1px 2px;
  border-spacing: 0;
  text-align: left;
  width: 21vw;
  }
tr {
  height:1.75em;
}
tbody tr {
  background-color: pink;
  transition: transform 1s;
  transition-timing-function: ease-in-out;
  transform: translateY(0px);
  }
th {
  text-align: center;
  padding: 0.2em;
  width: 7vw;
  border-bottom: 1px solid green;
  cursor: pointer;
  }   
td {
  padding: 0.2em;
  width: 7vw;
  border: solid green;
  border-width: 1px 0px 1px 0px;
  }
tr td:nth-child(2) {
  text-align: right;
  }
tr td:nth-child(3) {
  text-align: right;
  }
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>replit</title>
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script src="script.js" defer></script>
</head>

<body>
    <table id="sorted">
        <thead>
            <tr>
                <th id="coin">Coin</th>
                <th id="change">Change</th>
                <th id="price">Price USD</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td headers="coin">BTC</td>
                <td headers="change">2%</td>
                <td headers="price" data-price="47594.70"></td>
            </tr>
            <tr>
                <td headers="coin">ETH</td>
                <td headers="change">2.11%</td>
                <td headers="price" data-price="3972.46"></td>
            </tr>
            <tr>
                <td headers="coin">BNB</td>
                <td headers="change">0.1%</td>
                <td headers="price" data-price="538.72"></td>
            </tr>
            <tr>
                <td headers="coin">USDT</td>
                <td headers="change">0%</td>
                <td headers="price" data-price="0.9997"></td>
            </tr>
            <tr>
                <td headers="coin">SOL</td>
                <td headers="change">-2%</td>
                <td headers="price" data-price="187.39"></td>
            </tr>
            <tr>
                <td headers="coin">ADA</td>
                <td headers="change">1.67%</td>
                <td headers="price" data-price="1.29"></td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Any ideas, simplifications are most welcome.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.