Search code examples
htmljqueryhtml-tablecollapsable

How to collapse multiple table rows TR with jquery


I am trying to collapse table rows in table using jQuery.

There are 3 'levels', header, sub, and child in the table.

Since I am dealing with tr my normal approach with ul and li and won't work, since the table rows are not nested. I am consuming the tr from a service and unfortunately cannot change it to ul/li parent/child relation. Next challenge I also do not have parent-id and child-id relation. I just get a raw list of rows with some css classes and data-attributes.

In my code below collapsing the header (level 0) works fine. Problem happens with level 1 (click e.g. Sub 1.4), where some of the headers (level 0) will also be collapsed instead of only the Child (level 2).

Here is my code:

 $(".collapse").click(function() { // attach click handler to .collapse css class
   const level = $(this).parent("tr").data("level"); // 0 header, 1 sub, 2 child
   
   var state = "";
   $(this).find("span").text(function(_, value) {
     state = (value == '-' ? '+' : '-');
     return state;
   });

   console.log(level);

   var rows;
   
   if (level == 0) { // a header row (level=0) was clicked. collapse every tr until another header row is reached
     rows = $(this).parent("tr").nextUntil("[data-level='0']");
   } 
   else { // a sub row (level=1) was clicked. 
     rows = $(this).parent("tr").nextUntil("[data-level='" + level + "']"); // ## incorrect statement - what to do here to get the correct siblings "below" the sub?
   }

   // if state == - (expanded), then show, otherwise hide
   _ = state == "-" ? rows.show() : rows.hide();
 });
body {font-family:'Open Sans';font-size:20px}
th {border:solid 1px #000;text-align:left;font-weight:normal}
[data-level="0"] th {font-weight:bold;}
[data-level="1"] th {padding-left:20px;}
[data-level="2"] th {padding-left:40px;}
.collapse {cursor:pointer}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<body>
  <table>
    <tr data-level="0">
      <th scope="row" class="collapse"><span>-</span> Header 1</th>
    </tr>
    <tr data-level="1">
      <th scope="row" class="collapse"><span>-</span> Sub 1.1</th>
    </tr>
    <tr data-level="1">
      <th scope="row" class="collapse"><span>-</span> Sub 1.2</th>
    </tr>
    <tr data-level="1">
      <th scope="row" class="collapse"><span>-</span> Sub 1.3</th>
    </tr>
    <tr data-level="2">
      <th scope="row">Child 1.3.1</th>
    </tr>
    <tr data-level="1">
      <th scope="row" class="collapse"><span>-</span> Sub 1.4</th>
    </tr>    
    <tr data-level="0">
      <th scope="row" class="collapse"><span>-</span> Header 2</th>
    </tr>
    <tr data-level="1">
      <th scope="row" class="collapse"><span>-</span> Sub 2.2</th>
    </tr>
    <tr data-level="1">
      <th scope="row" class="collapse"><span>-</span> Sub 2.2</th>
    </tr>
    <tr data-level="2">
      <th scope="row">Child 2.2.1</th>
    </tr>
    <tr data-level="0">
      <th scope="row" class="collapse"><span>-</span> Header 3</th>
    </tr>
    <tr data-level="1">
      <th scope="row" class="collapse"><span>-</span> Sub 3.1</th>
    </tr>
    <tr data-level="2">
      <th scope="row">Child 3.1.1</th>
    </tr>    
  </table>
</body>

JSFiddle here: https://jsfiddle.net/cLnmragy/2/


Solution

  • The trick is do an OR in .nextUntil([selector]) filter, which yields the following solution:

    e.g.

         rows = $(this).parent("tr").nextUntil("[data-level='1'],[data-level='0']");
    

    Full javascript:

     $(".collapse").click(function() { // attach click handler to .collapse css class
       const level = $(this).parent("tr").data("level"); // 0 header, 1 sub, 2 child
       
       var state = "";
       $(this).find("span").text(function(_, value) {
         state = (value == '-' ? '+' : '-');
         return state;
       });
    
       console.log(level);
    
       var rows;
       
       if (level == 0) { // a header row (level=0) was clicked. collapse every tr until another header row is reached
         rows = $(this).parent("tr").nextUntil("[data-level='0']");
       } 
       else { // a sub row (level=1) was clicked. 
         rows = $(this).parent("tr").nextUntil("[data-level='1'],[data-level='0']");
       }
       // if state == - (expanded), then show, otherwise hide
       _ = state == "-" ? rows.show() : rows.hide();
     });