2

I'd like to implement the data-tables responsive behavior of hiding columns when the table's width decreases past a certain point (basically the point when the tr's element overflow would occur).

I know how I can collapse the rows: simply on window resize, check for when the width of the table is greater than the width of it's container #table-wrapper. When this happens, I hide the outer most table column and place it in a stack, while adding those values to each rows extended portion (that will be toggled to be visible, the way datatables does).

If the website is accessed while the window is in a small size, upon loading the table can check for the overflow condition (table.width > table-wrapper.width), and iterate through the outer most columns, hiding them and pushing them on to the stack until the overflow condition is false.

However, How can I bring the elements back? That is, when the table is growing, I'm not sure under what condition I can pop the columns off the stack and unhide them.

I'm thinking of getting the minimum-size of the table somehow, and upon window resize, check if the wrappers width is bigger than the minimum size of the table plus the minimum size of first item on the stack? However, I don't know how to get these minimum widths.

Is there a reliable way of getting these min.widths for any font size, or is there a better way you can recommend?

<table class="test">
     <tbody>

         <tr>
           <th> heading1 </th>
           <th> heading2 </th>
           <th> heading3 </th>
           <th> heading4 </th>
         </tr>
       
         <tr>
             <td>data1</td>
             <td>data2</td>
             <td>data3</td>
             <td>data4</td>
         </tr>

          <tr>
             <td>data1</td>
             <td>data2</td>
             <td>data3</td>
             <td>data4</td>
         </tr>

     </tbody>

</table>

*Update:

I thought a very unelegant solution where I could place an invisible copy of the table directly behind it, and upon the window resize event firing, I could continually add an invisible column to the invisible table and check for the overflow condition (table_border > wrapper_border). This seems really inefficient though...

I also learned that min-width for < td > elements are undefined. However, Its possible to place a or

inside each element that does have a min-width. For each column, the minimum with is equal to the that has the maximum length. So, I guess I could check all the data for each column and get the minimum length that way. The min. width of the table is what I need to base whether or not adding another column would cause an overflow.

What do you think; do you have any other ideas?

8
  • 4
    You said "the way datatables.js does"... why simply not using it? Commented Jun 27, 2020 at 15:50
  • It seems that you have a mechanism to hide outermost columns when the table is shrinking. Is it possible to simply make all columns visible and recalculate which columns to hide as the table grows (i.e. on any table resize)? Commented Jun 27, 2020 at 16:17
  • @DanieleRicci Because in my case, its been very buggy and sluggish, at least in the template I'm using. The documentation is great, however every time I try to use a feature, its headache to get it to work, despite the documentation making it look so simple. I figure I might as well spend all that effort into making a small, custom solution that will be less bloated, more customizable and better suted to what I'm doing. Commented Jun 27, 2020 at 20:06
  • @mankowitz Thats an interesting idea. It might have the undesireable effect of flashing the full table before the columns are added; but I'm going to look into this (or a variant) thanks! Commented Jun 27, 2020 at 20:07
  • How about checking to see if a horizontal scrollbar was forced to be displayed (whole page, or, better, div that contains the table)? If so, loop through the rows and set the last th/tds style.visibility="hidden" or something similar. keep looping until scrollbar is gone. Commented Jun 29, 2020 at 0:53

2 Answers 2

1
+300

One approach is to have data using white-space: nowrap elements inside the table cells and calculate the min width of each row which it will be the max width of the row data elements. These "break-widths" will be calculated only once when the table is filled with data (on load or re-new the data). Having these "break-widths", we can calculate the table minimum width and compare it with the body client width and hide columns accordingly on window resize.

Calculating the "break-widths"

This is performed on loading or each time table updates with new data (i.e. pagination page change). We go through table cell elements TH and TD and we're storing the biggest cell text width of the span elements.

function calcBreakWidths() {
  // Each column one base index
  let thIndex = 1;

  for (let th of document.querySelectorAll('thead th')) {
    // Get the width of every text span of every TH column,
    // this way, we'll have also header break-widths
    let breakWidth = document.querySelector(`thead th:nth-child(${thIndex}) > span`).offsetWidth;

    // Go through all column TD elements and keep the biggest text span width
    for (let span of document.querySelectorAll(`tbody td:nth-child(${thIndex}) > span`)) {
      if (span.offsetWidth > breakWidth) {
        breakWidth = span.offsetWidth;
      }
    }

    // Save the biggest text span break-width to the TH dataset
    th.dataset.breakwidth = breakWidth;

    // Next column index
    thIndex++;
  }
}

Hide / Show columns on window resize

On window.resize we calculate the table minimum widths by summing up all break-widths. Then we go through each column, starting from the most right one, and we check if the table min width including the current column is exceeded the body client width; if it did, then we hide the current column else we show the current column. This is done by using classList.add and classList.remove using the class named hidden with display: none CSS style. At the end of each column iteration, we subtract the current column break-width from the table minimum width so to have the next correct table minimum width without the current column break-width.

window.addEventListener('resize', function() {
  const bodyWidth = document.body.clientWidth;

  // Get all columns break-widths from the TH element dataset
  const breakWidths = [...document.querySelectorAll('th')]
    .map(th => parseInt(th.dataset.breakwidth));

  // Sum-up all column break-widths (+2 pixels) to calculate table minimum width
  let tableMinWidth = breakWidths
    .reduce((total, breakWidth) => total + breakWidth + 2, 0);

  for (let column = breakWidths.length; column > 0; column--) {
    const tableIsLarger = tableMinWidth > bodyWidth;

    // const th = document.querySelector(`th:nth-child(${column})`);
    const cells = document.querySelectorAll(`th:nth-child(${column}), td:nth-child(${column})`);

    // If table min width is larger than body client width,
    // then hide the current column
    if (tableMinWidth > bodyWidth) {
      // We're hidding the column by iterating on each table cell
      // and add the hidden class only if the column does not already contain
      // the hidden class. We're doing this for performance reasons
      if (!cells[0].classList.contains('hidden')) {
        cells.forEach(cell => cell.classList.add('hidden'));
      }
    // Else if the table min width is not larger than body client width,
    // we remove the hidden class from the column to show each column cell
    } else if (cells[0].classList.contains('hidden')) {
      cells.forEach(cell => cell.classList.remove('hidden'));
    }

    // Subtract current column break-width from the total table min width
    // so to have the correct min table width for the next column
    tableMinWidth -= breakWidths[column - 1] + 2;
  }
});

The snippet shows this in action

Please read inline comments

// Each TH class is a field name
const fields = [...document.querySelectorAll('thead th')].map(el => el.className);

// Generate 20 rows with fake data
for (let i = 0; i <= 20; i++) {
  const tr = document.createElement('tr');

  fields.forEach(field => {
    const td = document.createElement('td');
    const text = document.createElement('span');
    td.className = field;
    text.textContent = fake(field);
    td.appendChild(text);
    tr.appendChild(td);
  });

  document.querySelector('table tbody').appendChild(tr);
}

// Calculate each column break width, the max data span element width
function calcBreakWidths() {
  let thIndex = 1;

  for (let th of document.querySelectorAll('thead th')) {
    let breakWidth = document.querySelector(`thead th:nth-child(${thIndex}) > span`).offsetWidth;
    for (let span of document.querySelectorAll(`tbody td:nth-child(${thIndex}) > span`)) {
      if (span.offsetWidth > breakWidth) {
        breakWidth = span.offsetWidth;
      }
    }

    th.dataset.breakwidth = breakWidth;
    thIndex++;
  }
}

calcBreakWidths();

// Handle window resize and hide each column exceeds BODY client width 
window.addEventListener('resize', function() {
  const bodyWidth = document.body.clientWidth;

  // Get the break widths saved to the TH datasets
  const breakWidths = [...document.querySelectorAll('th')]
    .map(th => parseInt(th.dataset.breakwidth));

  // Calculate table min width (+2 pixels for border + padding for each cell)
  let tableMinWidth = breakWidths
    .reduce((total, breakWidth) => total + breakWidth + 2, 0);

  // Loop from last to the first column and compare the widths
  for (let column = breakWidths.length; column > 0; column--) {
    const cells = document.querySelectorAll(`th:nth-child(${column}), td:nth-child(${column})`);

    if (tableMinWidth > bodyWidth) {
      if (!cells[0].classList.contains('hidden')) {
        cells.forEach(cell => cell.classList.add('hidden'));
      }
    } else if (cells[0].classList.contains('hidden')) {
      cells.forEach(cell => cell.classList.remove('hidden'));
    }

    tableMinWidth -= breakWidths[column - 1] + 2;
  }
});

// Function to create fake data
function fake(what) {
  switch (what) {
    case 'name': return faker.name.findName();
    case 'email': return faker.internet.exampleEmail();
    case 'address': return faker.address.streetAddress();
    case 'country': return faker.address.country();
    case 'account': return faker.finance.accountName();
  }
}
table {
  border-collapse: collapse;
  width: 100%;
}

table, th, td {
  border: 1px solid black;
}

tbody td > span,
thead th > span {
  white-space: nowrap;
  background-color: gold;
}

thead th > span {
  background-color: aquamarine;
}

.hidden {
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/locales/en/faker.en.min.js" crossorigin="anonymous"></script>

<table>
  <thead>
    <tr>
      <th class="name"><span>Name<span></th>
      <th class="email"><span>Email</span></th>
      <th class="account"><span>Personal or Business Account</span></th>
      <th class="address"><span>Personal or Business Address</span></th>
      <th class="country"><span>Country</span></th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

You can run the above code snippet and try to resize the window, but you'll have a better view of the table responsive behavior if you try to resize the browser after opening the snippet in Full page or by navigating to the full example page here: https://zikro.gr/dbg/so/62491859/responsive-table.html

It's not perfect but it runs smoothly on my browser and it hides columns from right to left based on columns data text width.

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks so much! When you say it's not perfect, what problems are you saying it has? Also I noticed that you're calculating the break-widths on the span elements, but what if the header of that column is the largest value? That would mean that row's header is the minimum width. I'm a beginner in js, so please bear with me...
In regards to adding the columns back, I also thought of simply taking the width of the column while it was still hidden and checking whether or not adding it to the current table width would create an overflow condition. However, when I tested the size of a particular column hidden vs. un-hidden the sizes were different...
@Jaigus you just pointed out some of its' in-perfections. It does not calculate header break-widths and also it does not take account for different data context like images, line-breaks etc. For adding the columns back, we don't have to worry or calculate new widths because we're doing this at the very beginning when all cells and columns are visible and we're doing this for accuracy and also for performance reasons.
I see. For this situation (sorry I didn't mention before) this will be plain text data with no images and no line breaks (everything will be set to nowrap). The only thing is I will have to run this calculation again considering pagination, but that's okay. How are you actually achieving bringing the columns back? And as for the headers, can't we just compare them at the end of the calculation?
@Jaigus the calculation is not an intense process and it will happen for every page. You have to also check the performance on mobile devices which I believe will be efficient enough for 50 rows maximum and 6 to 7 columns, maybe more. Yes you are right, the header calculation is also important, I will update my answer later with header calculation and more detailed explanation on how columns hide and show again.
|
0

Could you give an example of the desired behavior? If you're talking about this datatables.js behavior: https://www.datatables.net/examples/styling/semanticui.html, then the simple answer would be this CSS Rule https://www.w3schools.com/cssref/css3_pr_mediaquery.asp

@media only screen and (max-width: 600px) {
  table td:nth-child(5) {
    display: none;
    visibility:hidden;
  }
}
@media only screen and (max-width: 700px) {
  table td:nth-child(6) {
    display: none;
    visibility:hidden;
  }
}

(you get the idea)

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.