skip to Main Content

I have a Vue app calculating a table with rowspans. The algorithm calculates the data ( and rowspans ) based on a configuration file so the application only renders the columns ( and the column order ) based on the calculated result.

Given the following sample ( Reproduction link )

<template>
  <table>
    <thead>
      <th>City</th>
      <th>Inhabitant</th>
      <th>House</th>
      <th>Room</th>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in tableMatrix" :key="rowIndex">
        <template v-for="(cell, columnIndex) in row" :key="columnIndex">
          <td v-if="cell.isCoveredByPreviousCell" style="display: none" />
          <td v-else :rowspan="cell.rowspan ?? 1">
            <template v-if="cell.content">
              {{ cell.content }}
            </template>
          </td>
        </template>
      </tr>
    </tbody>
  </table>
</template>

<script setup lang="ts">
import { ref, Ref } from 'vue';

interface Cell { isCoveredByPreviousCell: boolean; rowspan: number; content?: string; }

type TableMatrix = Cell[][];

const tableMatrix: Ref<TableMatrix> = ref([
  [
    { isCoveredByPreviousCell: false, rowspan: 5, content: "City 1" },
    { isCoveredByPreviousCell: false, rowspan: 4, content: "Inhabitant 1" },
    { isCoveredByPreviousCell: false, rowspan: 3, content: "House 1" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Room 2" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Room 3" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "House 2" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ],
  [
    { isCoveredByPreviousCell: true },
    { isCoveredByPreviousCell: false, content: "Inhabitant 2" },
    { isCoveredByPreviousCell: false, content: "House 3" },
    { isCoveredByPreviousCell: false, content: "Room 1" },
  ]
])
</script>

<style>
table, th, td { border-collapse: collapse; border: 1px solid black; }
</style>

I get the following ( correct ) output

enter image description here

It’s quite hard to identify a single "line", I’m looking for a way to make this more clear. A zebra striped table won’t work for the table design. Maybe I need to add "dividing" rows with a fixed height and a different background color. Or increase the border bottom width of row cells.

I tried to add tr { border-bottom: 5px solid black; } ( reproduction link ) but then I get the following output

enter image description here

I also tried to add a dividing row ( reproduction link )

    <tr>
      <template v-for="(cell, columnIndex) in row" :key="columnIndex">
        <td style="background: red">divider</td>
      </template>
    </tr>

but get this result

enter image description here

Do you have any ideas?

6

Answers


  1. I think that it’s messy, both visually and in the code, to have it implemented with rowspans. This is because you want two cells on top of each other to be in the same row, but that’s not how it’s implemented, so any solution that makes them appear to be in the same row will be messy

    This is a matter of opinion, but I think that you should put those stacked cells in the same table row, then use CSS to put one piece of information above the other in the same cell. Then you can use the tr { border-bottom: 5px solid black; } to get the result that you want.

    Login or Signup to reply.
  2. An implementation of my comment. First with a blurred green zebra striping:

    const data = [
      [{
          isCoveredByPreviousCell: false,
          rowspan: 5,
          content: "City 1"
        },
        {
          isCoveredByPreviousCell: false,
          rowspan: 4,
          content: "Inhabitant 1"
        },
        {
          isCoveredByPreviousCell: false,
          rowspan: 3,
          content: "House 1"
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 1"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 2"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 3"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "House 2"
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 1"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "Inhabitant 2"
        },
        {
          isCoveredByPreviousCell: false,
          content: "House 3"
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 1"
        },
      ]
    ];
    const tbody = document.querySelector('table tbody');
    let rowIndex = 0;
    for (let r of data) {
      let row = document.createElement('tr');
      row.classList.toggle('even', rowIndex % 2 === 0);
      row.classList.toggle('odd', rowIndex % 2 !== 0);
      for (let c of r) {
        let cell = document.createElement('td');
        if (c.content) {
          cell.appendChild(document.createTextNode(c.content));
        }
        row.appendChild(cell);
      }
      tbody.appendChild(row);
      rowIndex++;
    }
    tbody.parentElement.classList.add('zebra');
    const dataTable = tbody.parentElement.cloneNode(true);
    document.body.appendChild(dataTable);
    dataTable.classList.remove('zebra');
    const dataTbody = dataTable.querySelector('tbody');
    dataTbody.innerHTML = "";
    rowIndex = 0;
    for (let r of data) {
      let row = document.createElement('tr');
      for (let c of r) {
        if (!c.isCoveredByPreviousCell) {
          let cell = document.createElement('td');
          if (c.rowspan && c.rowspan > 0) {
            cell.setAttribute('rowspan', c.rowspan);
          }
          if (c.content) {
            cell.appendChild(document.createTextNode(c.content));
          }
          row.appendChild(cell);
        }
      }
      dataTbody.appendChild(row);
      rowIndex++;
    }
    .as-console-wrapper { max-height: 44px; height: 44px; }
    
    .zebra .even {
      background-color: white;
      color: white;
    }
    
    .zebra .odd {
      background-color: palegreen;
      color: palegreen;
    }
    
    .zebra th {
      background-color: white;
      color: white;
    }
    
    .zebra {
      border-collapse: collapse;
      border: 1px solid palegreen;
      position: absolute;
      z-index: 0;
      filter: blur(4px);
    }
    
    table:not(.zebra) {
      border-collapse: collapse;
      border: 1px solid;
      position: absolute;
      z-index: 1;
    }
    table:not(.zebra) td {
      border: 1px solid;
    }
    <table>
      <thead>
        <th>City</th>
        <th>Inhabitant</th>
        <th>House</th>
        <th>Room</th>
      </thead>
      <tbody>
      </tbody>
    </table>

    Then with blurred colored bottom borders on the background table. I tend to like this one better.

    const data = [
      [{
          isCoveredByPreviousCell: false,
          rowspan: 5,
          content: "City 1"
        },
        {
          isCoveredByPreviousCell: false,
          rowspan: 4,
          content: "Inhabitant 1"
        },
        {
          isCoveredByPreviousCell: false,
          rowspan: 3,
          content: "House 1"
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 1"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 2"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 3"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "House 2"
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 1"
        },
      ],
      [{
          isCoveredByPreviousCell: true
        },
        {
          isCoveredByPreviousCell: false,
          content: "Inhabitant 2"
        },
        {
          isCoveredByPreviousCell: false,
          content: "House 3"
        },
        {
          isCoveredByPreviousCell: false,
          content: "Room 1"
        },
      ]
    ];
    const tbody = document.querySelector('table tbody');
    let rowIndex = 0;
    for (let r of data) {
      let row = document.createElement('tr');
      row.classList.toggle('even', rowIndex % 2 === 0);
      row.classList.toggle('odd', rowIndex % 2 !== 0);
      for (let c of r) {
        let cell = document.createElement('td');
        if (c.content) {
          cell.appendChild(document.createTextNode(c.content));
        }
        row.appendChild(cell);
      }
      tbody.appendChild(row);
      rowIndex++;
    }
    tbody.parentElement.classList.add('zebra');
    const dataTable = tbody.parentElement.cloneNode(true);
    document.body.appendChild(dataTable);
    dataTable.classList.remove('zebra');
    const dataTbody = dataTable.querySelector('tbody');
    dataTbody.innerHTML = "";
    rowIndex = 0;
    for (let r of data) {
      let row = document.createElement('tr');
      for (let c of r) {
        if (!c.isCoveredByPreviousCell) {
          let cell = document.createElement('td');
          if (c.rowspan && c.rowspan > 0) {
            cell.setAttribute('rowspan', c.rowspan);
          }
          if (c.content) {
            cell.appendChild(document.createTextNode(c.content));
          }
          row.appendChild(cell);
        }
      }
      dataTbody.appendChild(row);
      rowIndex++;
    }
    .as-console-wrapper { max-height: 44px; height: 44px; }
    
    .zebra .even {
      background-color: white;
      color: white;
    }
    
    .zebra .odd {
      background-color: white;
      color: white;
    }
    
    .zebra th {
      background-color: white;
      color: white;
    }
    
    .zebra td {
      border: 1px solid palegreen;
    }
    .zebra {
      border-collapse: collapse;
      border: 1px solid palegreen;
      position: absolute;
      z-index: 0;
      filter: blur(2px);
    }
    
    table:not(.zebra) {
      border-collapse: collapse;
      border: 1px solid;
      position: absolute;
      z-index: 1;
    }
    table:not(.zebra) td {
      border: 1px solid;
    }
    <table>
      <thead>
        <th>City</th>
        <th>Inhabitant</th>
        <th>House</th>
        <th>Room</th>
      </thead>
      <tbody>
      </tbody>
    </table>
    Login or Signup to reply.
  3. thing you are wanting to have like zebra strips tabe
    you should have something in corresponding to every cell like you have city 2, inhabitant 1 and house 1 and in room there is some data as well but in the first row there only is on td in that row there is not other td corresponding to it and now even if you want to have that zebra kinda of table then the styling will only occur in the corresponding td like the room 1 will only have a color because there is no other td corresponding to it

    your code also seems a bit messy using the rowspan in js like it can be done through the html as well One of the main thing in the coding is that to keep your code as short as possible

    and if you want it to be like zebra striped here’s the code

    <template>
      <table>
        <thead>
          <th>City</th>
          <th>Inhabitant</th>
          <th>House</th>
          <th>Room</th>
        </thead>
        <tbody>
          <tr v-for="(row, rowIndex) in tableMatrix" :key="rowIndex">
            <template v-for="(cell, columnIndex) in row" :key="columnIndex">
              <td v-if="cell.isCoveredByPreviousCell" style="display: none" />
              <td v-else :rowspan="cell.rowspan ?? 1">
                <template v-if="cell.content">
                  {{ cell.content }}
                </template>
              </td>
            </template>
          </tr>
        </tbody>
      </table>
    </template>
    
    <script setup lang="ts">
    import { ref, Ref } from 'vue';
    
    interface Cell { isCoveredByPreviousCell: boolean; rowspan: number; content?: string; }
    
    type TableMatrix = Cell[][];
    
    const tableMatrix: Ref<TableMatrix> = ref([
      [
        { isCoveredByPreviousCell: false, rowspan: 5, content: "City 2" },
        { isCoveredByPreviousCell: false, rowspan: 4, content: "Inhabitant 1" },
        { isCoveredByPreviousCell: false, rowspan: 3, content: "House 1" },
        { isCoveredByPreviousCell: false, content: "Room 1" },
      ],
      [
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: false, content: "Room 2" },
      ],
      [
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: false, content: "Room 3" },
      ],
      [
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: false, content: "House 2" },
        { isCoveredByPreviousCell: false, content: "Room 1" },
      ],
      [
        { isCoveredByPreviousCell: true },
        { isCoveredByPreviousCell: false, content: "Inhabitant 2" },
        { isCoveredByPreviousCell: false, content: "House 3" },
        { isCoveredByPreviousCell: false, content: "Room 1" },
      ]
    ])
    </script>
    <style>
    table {
      border-collapse: collapse;
    }
    th
    th, td {
      padding: 5px;
      text-align: left;
      border-bottom: 2px solid #ddd;
    }
    
    tr:nth-child(even) {
      background-color: #f2f2f2;
    }
    
    tr:nth-child(odd) {
      background-color: #E020;
    }
    
    tr:nth-child(4n+1) {
      background-color: #FFCAD2; /* color for House 1 row */
    }
    
    tr:nth-child(3n+2) {
      
      background-color: #B2EBF2; /* color for Room 2 row */
    }
    
    tr:nth-child(3n+3) {
      background-color: #F8G4; /* default color for other rows */
    }
    </style>
    
    
    Login or Signup to reply.
  4. Not really a code answer, more a frame challenge:

    Must it be rows? Instead you can use arrows to point from div to div (example code)

    example of connecting

    This aproach is very similar to what Google does in its Cloud Platform, where they have to solve the same issue you have. Of course this is not the most pretty drawing, its an example. You could have straight lines with 90deg turns, or everything the same colour and/or dotted.

    If you wrap your current text in a span with an border and use the tables vertical align, you are very close to the example above "only" needing to connect the dots.

    Login or Signup to reply.
  5. I think the issue is that you don’t really have lines or rows anymore, but rather nested tiles, which is why zebra stripes won’t work. I think in this case, it makes more sense to highlight these tiles and their nesting level. You can do this by adjust the borders to the level, i.e. use a thick border between cities, use a thin border between rooms:

    Adjusting the vertical borders gives you these ⊢ shapes, which outline the tiles more clearly. Add some colors for a more intuitive distinction between structure and content:

    enter image description here

    Doubling the size of the borders makes the structure even more apparent, but you need to check if it works with the rest of your page:

    enter image description here

    To figure out the top border width of a row, you just have to find the index of the first data cell:

    const rowClasses = computed(() => tableMatrix.value.map(row => 'border-width-'+ (4 - row.findIndex(cell => !cell.isCoveredByPreviousCell))))
    

    You can set it as a class on each row.

    Here it is in the playground

    Login or Signup to reply.
  6. The problem

    OP wants the table’s primary rows (the most tall ones) to be clearly wisible
    Here’s the OP’s image provided in comment:
    https://i.imgur.com/Vt6JouO.png

    The solution

    This is easy to implement by adding a top border for those rows

    <template>
      <table>
        <tr v-for="(row, rowIndex) in tableMatrix"
              :class="{split: isFullSplit(row)}"> ... </tr>
      </table>
    </template>
    <script setup lang="ts">
      function isFullSplit(row: Cell[]) {
        return row.every(c => !c.isCoveredByPreviousCell)
      }
    </script>
    <style>
      /* make a big border-top for the primary rows  */
      tr:not(:first-child).split { border-top: 10px solid red; }
    </style>
    

    playground
    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search