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/
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();
});