Search code examples
javascripthtmlhtml-tablemerge

Merge neighbouring HTML table cells with same value using JS


I've been wrestling with this for a while. I have a table which is automatically generated based on some JSON data, which may vary. I'd like to merge neighbouring cells in the first column which have the same value, e.g. "fish" and "bird" in this table:

<table>
  <tr>
    <td>fish</td>
    <td>salmon</td>
  </tr>
  <tr>
    <td>fish</td>
    <td>cod</td>
  </tr>
  <tr>
    <td>fish</td>
    <td>plaice</td>
  </tr>
  <tr>
    <td>bird</td>
    <td>robin</td>
  </tr>
  <tr>
    <td>bird</td>
    <td>crow</td>
  </tr>
</table>

I don't want to use any libraries ideally, just pure JS.

This is what I would like it to look like:

table, tr, td {
  border: solid 1px black;
}
<table>
  <tr>
    <td rowspan="3">fish</td>
    <td>salmon</td>
  </tr>
  <tr>
    <td>cod</td>
  </tr>
  <tr>
    <td>plaice</td>
  </tr>
  <tr>
    <td rowspan="2">bird</td>
    <td>robin</td>
  </tr>
  <tr>
    <td>crow</td>
  </tr>
</table>

I've been finding different ways to identify the different values and their frequency and then change the rowspan to the right number and subsequently deleting the the other cells but these all broke down in differing use cases.

This is what I have so far:

let table = document.querySelector('table');
let rowCount = 1;

for (let i = 0; i < (table.rows.length - 1); i++) {
  if (table.rows[i].cells[0].innerHTML === table.rows[i + 1].cells[0].innerHTML) {
    rowCount++;
  } else if (rowCount !== 1) {
    table.rows[i].cells[0].setAttribute('rowspan', rowCount);
    for (let j = (i - rowCount + 1); j < rowCount; j++) {
      table.rows[j].cells[0].remove();
    };
    rowCount = 1;
  };
};
table, tr, td {
  border: solid 1px black;
}
<table>
  <tr>
    <td>fish</td>
    <td>salmon</td>
  </tr>
  <tr>
    <td>fish</td>
    <td>cod</td>
  </tr>
  <tr>
    <td>fish</td>
    <td>plaice</td>
  </tr>
  <tr>
    <td>bird</td>
    <td>robin</td>
  </tr>
  <tr>
    <td>bird</td>
    <td>crow</td>
  </tr>
</table>

This isn't doing what I want at all but I feel I'm really close! It's trying to count the number of (first column) cells for which the one below has the same value, assigning this number to the rowspan of the last relevant cell and then deleting the subsequent cells before looping back to catch the rest of them. I'd love for my final code to be a variation of this, so can someone show me where I'm going wrong please?


Solution

  • You were indeed pretty close!

    A way to simplify quite a bit is to keep a reference to the current "header" cell, i.e. the one you want to increase the rowspan of. That way you don't have to deal with indexes at all, yielding a very straightforward algorithm:

    • For each row

      • Set firstCell to the row's first cell
      • If this is the first row OR firstCell's text is different from headerCell's text

        • Set headerCell to firstCell
      • Otherwise
        • Increase headerCell's rowSpan by 1
        • Remove firstCell

    In JavaScript, it looks like this:

    const table = document.querySelector('table');
    
    let headerCell = null;
    
    for (let row of table.rows) {
      const firstCell = row.cells[0];
      
      if (headerCell === null || firstCell.innerText !== headerCell.innerText) {
        headerCell = firstCell;
      } else {
        headerCell.rowSpan++;
        firstCell.remove();
      }
    }
    table, tr, td {
      border: solid 1px black;
    }
    <table>
      <tr>
        <td>fish</td>
        <td>salmon</td>
      </tr>
      <tr>
        <td>fish</td>
        <td>cod</td>
      </tr>
      <tr>
        <td>fish</td>
        <td>plaice</td>
      </tr>
      <tr>
        <td>bird</td>
        <td>robin</td>
      </tr>
      <tr>
        <td>bird</td>
        <td>crow</td>
      </tr>
    </table>