njeoo
njeoo

Reputation: 43

Recursive HTML table tree using only JavaScript

I developed clicking on the parent node to display its child row. I just need to enable to click on the child data should open its sub child rows as recursive one or table tree. Could anyone add your logic which will help me to understand and help the same to others?

document.getElementById("products").addEventListener("click", function(e) {
    if (e.target.tagName === "A") {
        e.preventDefault();
        var row = e.target.parentNode.parentNode;
        while ((row = nextTr(row)) && !/\bparent\b/ .test(row.className))
            toggle_it(row);
    }
});

function nextTr(row) {
    while ((row = row.nextSibling) && row.nodeType != 1);
    return row;
}

function toggle_it(item){ 
     if (/\bopen\b/.test(item.className))
         item.className = item.className.replace(/\bopen\b/," ");
     else
         item.className += " open";
 } 
tbody tr {
    display : none;
}
tr.parent {
    display : table-row;
}
tr.open {
    display : table-row;
}
<!-- 
  Bootstrap docs: https://getbootstrap.com/docs
-->

<div class="container">
  <table class="table" id="products">
    <thead>
    <tr>
        <th>Product</th>
        <th>Price</th>
        <th>Destination</th>
        <th>Updated on</th>
    </tr>
    </thead>
    <tbody>
    <tr class="parent">
    <td><a href="#">+</a>Oranges</td>
        <td>100</td>
        <td>On Store</td>
        <td>22/10</td>
    </tr>
    <tr>
        <td>121</td>
        <td>120</td>
        <td>City 1</td>
        <td>22/10</td>
    </tr>
    <tr>
    <td>212</td>
        <td>140</td>
        <td>City 2</td>
        <td>22/10</td>
    </tr>
    <tr>
    <td>212</td>
        <td>140</td>
        <td>City 2</td>
        <td>22/10</td>
    </tr>
    <tr class="parent">
        <td><a href="#">+</a>Apples</td>
        <td>100</td>
        <td>On Store</td>
        <td>22/10</td>
    </tr>
    <tr>
        <td>120</td>
        <td>120</td>
        <td>City 1</td>
        <td>22/10</td>
    </tr>
    <tr>
        <td>120</td>
        <td>140</td>
        <td>City 2</td>
        <td>22/10</td>
    </tr>
    </tbody>
</table>
   
</div>

Upvotes: 2

Views: 3047

Answers (1)

Takit Isy
Takit Isy

Reputation: 10091

Updated answer

I've changed almost everything and simplified the code:

  • The toggle buttons are added automatically,
  • + changes to - when the parent is opened,
  • The table, the classes for opened and visible elements, and the buttons are passed as parameters,
  • It could be used on multiple table,

I've created a repository with that code on GitHub:
https://github.com/TakitIsy/table-to-tree

/* ---- < MAIN FUNCTION > ---- */
function tableToTree(table_Selector, tr_OpenedClass, tr_VisibleClass, tr_ToggleButton) {

  // Table elements variables
  var table = document.querySelector(table_Selector);
  var trs = document.querySelectorAll(table_Selector + " tr");

  // Add the buttons above the table
  var buttons = document.createElement('div');
  buttons.innerHTML = '<button>[‒] All</button><button>[+] All</button>';
  table.insertBefore(buttons, table.childNodes[0]);
  buttons = buttons.querySelectorAll('button');
  // Add the actions of these buttons
  buttons[0].addEventListener("click", function() {
    trs.forEach(function(elm) {
      elm.classList.remove(tr_OpenedClass);
      elm.classList.remove(tr_VisibleClass);
    });
  });
  buttons[1].addEventListener("click", function() {
    trs.forEach(function(elm) {
      if (elm.innerHTML.includes(tr_ToggleButton))
        elm.classList.add(tr_OpenedClass);
      elm.classList.add(tr_VisibleClass);
    });
  });

  // Next tr function
  function nextTr(row) {
    while ((row = row.nextSibling) && row.nodeType != 1);
    return row;
  }

  // On creation, automatically add toggle buttons if the tr has childs elements
  trs.forEach(function(tr, index) {
    if (index < trs.length - 1) {
      if (+tr.getAttribute("level") < +trs[index + 1].getAttribute("level")) {
        var elm1 = tr.firstElementChild;
        elm1.innerHTML = tr_ToggleButton + elm1.innerHTML;
      }
    }
  });

  // Use the buttons added by the function above
  table.addEventListener("click", function(e) {
    
    // Event management
    if (!e) return;
    if (e.target.outerHTML !== tr_ToggleButton) return;
    e.preventDefault();
    
    // Get the parent tr and its level
    var row = e.target.closest("tr");
    row.classList.toggle(tr_OpenedClass);
    var lvl = +(row.getAttribute("level"));

    // Loop to make childs visible/hidden
    while ((row = nextTr(row)) && ((+(row.getAttribute("level")) == (lvl + 1)) || row.className.includes(tr_VisibleClass))) {
      row.classList.remove(tr_OpenedClass);
      row.classList.toggle(tr_VisibleClass);
    }
  });

}
/* ---- </ MAIN FUNCTION > ---- */

// Call the above main function to make the table tree-like
tableToTree('#myTable', 'opened', 'visible', '<span class="toggle"></span>');
tbody tr {
  display: none;
}

tr[level="0"],
tr.visible {
  display: table-row;
}

td {
  background: #ccc;
  padding: 4px 8px 4px 32px;
  text-align: left;
}

tr[level="1"] td {
  background: #ddd;
  padding-left: 40px;
}

tr[level="2"] td {
  background: #eee;
  padding-left: 48px;
}

tr .toggle {
  position: absolute;
  left: 16px;
  cursor: pointer;
}

.toggle::after {
  content: "[+]";
}

.opened .toggle::after {
  content: "[‒]";
}
<table id="myTable">
  <tbody>
    <tr level="0">
      <td>Parent 1</td>
    </tr>
    <tr level="1">
      <td>Match 1</td>
    </tr>
    <tr level="1">
      <td>Match 2</td>
    </tr>
    <tr level="0">
      <td>Parent 2</td>
    </tr>
    <tr level="1">
      <td>Mismatch 1</td>
    </tr>
    <tr level="1">
      <td>Mismatch 2</td>
    </tr>
    <tr level="2">
      <td>Mismatch 2.1</td>
    </tr>
  </tbody>
</table>
<br>


⋅ ⋅ ⋅

Old answer

I played a little with your code…
Trying to use as many as possible of existing functions/methods to make the code cleaner and easier to read and understand.

… and ended-up with that snippet:
(See comments in my code for more details)

document.getElementById("products").addEventListener("click", function(e) {
  // I think using the not equal is nicer here, just my opinion… Less indenting.
  if (!e) return;                       // Exit if null
  if (e.target.tagName !== "A") return; // Exit if not A element

  e.preventDefault();
  var row = e.target.closest("tr"); // Better than parent parent!
  var cls = row.classList[0];       // Get the first class name (the only one in our html)
  var lvl = +(cls.slice(-1)) + 1;   // Unary operator +1 on the last character
  cls = cls.slice(0, -1) + lvl;     // Replace the last char with lvl to get the class to be toggled

  // Check if the row is of the class to be displayed OR if the row is already opened
  // (so that clicking close on parent also closes sub-childs)
  while ((row = nextTr(row)) && (row.className.includes(cls) || row.className.includes("open")))
    row.classList.toggle("open"); // Better than the function you created, it already exists!
});

function nextTr(row) {
  while ((row = row.nextSibling) && row.nodeType != 1);
  return row;
}

// Select all the tr childs elements (all levels except 0
var allChilds = document.querySelectorAll("tr[class^=level]:not(.level0)");
// Added the below for buttons after comments
document.getElementById("openAll").addEventListener("click", function() {
  allChilds.forEach(function(elm){
    elm.classList.add("open");
  });
});
document.getElementById("closeAll").addEventListener("click", function() {
  allChilds.forEach(function(elm){
    elm.classList.remove("open");
  });
});
tbody tr {
  display: none;
}


/* Simplified */

tr.level0,
tr.open {
  display: table-row;
}


/* Added colors for better visibility */

tr.level0 {
  background: #ccc;
}

tr.level1 {
  background: #ddd;
}

tr.level2 {
  background: #eee;
}


/* Added some more styling after comment */

tr td {
  padding: 0.2em 0.4em;
}

tr td:first-of-type {
  position: relative;
  padding: 0.2em 1em;
}

tr td a {
  color: inherit;
  /* No special color */
  text-decoration: inherit;
  /* No underline */
  position: absolute;
  left: 0.25em;
}

tr.level1 td:first-of-type {
  padding-left: 1.5em;
}

tr.level2 td:first-of-type {
  padding-left: 2em;
}
<button id="openAll">+ All</button>
<button id="closeAll">- All</button>
<table class="table" id="products">
  <thead>
    <tr>
      <th>Product</th>
      <th>Price</th>
      <th>Destination</th>
      <th>Updated on</th>
    </tr>
  </thead>
  <tbody>
    <tr class="level0">
      <td><a href="#">+</a>Oranges</td>
      <td>100</td>
      <td>On Store</td>
      <td>22/10</td>
    </tr>
    <tr class="level1">
      <td>121</td>
      <td>120</td>
      <td>City 1</td>
      <td>22/10</td>
    </tr>
    <tr class="level1">
      <td><a href="#">+</a>212</td>
      <td>140</td>
      <td>City 2</td>
      <td>22/10</td>
    </tr>
    <tr class="level2">
      <td>212</td>
      <td>140</td>
      <td>City 2</td>
      <td>22/10</td>
    </tr>
    <tr class="level2">
      <td>212</td>
      <td>140</td>
      <td>City 2</td>
      <td>22/10</td>
    </tr>
    <tr class="level0">
      <td><a href="#">+</a>Apples</td>
      <td>100</td>
      <td>On Store</td>
      <td>22/10</td>
    </tr>
    <tr class="level1">
      <td>120</td>
      <td>120</td>
      <td>City 1</td>
      <td>22/10</td>
    </tr>
    <tr class="level1">
      <td><a href="#">+</a>120</td>
      <td>140</td>
      <td>City 2</td>
      <td>22/10</td>
      <tr class="level2">
        <td>120</td>
        <td>140</td>
        <td>City 2</td>
        <td>22/10</td>
      </tr>
      <tr class="level2">
        <td>120</td>
        <td>140</td>
        <td>City 2</td>
        <td>22/10</td>
      </tr>
  </tbody>
</table>

I hope it helps!

Upvotes: 2

Related Questions