Skip to content

Instantly share code, notes, and snippets.

@ianchanning
Last active February 14, 2020 16:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ianchanning/7412377 to your computer and use it in GitHub Desktop.
Save ianchanning/7412377 to your computer and use it in GitHub Desktop.
Pure JavaScript hierarchical table row toggling (plus older jQuery version too)
/**
* Hierarchical table rows object
*
* Pure JavaScript - but comes with a IE9+ warning
*
* @param {object} options tableSelector e.g. 'table'
* parentClass e.g. 'parent'
* collapsedClass e.g. 'hidden'
* childClassPrefix e.g. 'parentId'
* @type function
* @return object
* @licence MIT
*/
var treeTable = function (options) {
'use strict';
var tableSelector = (options && options.tableSelector) || 'table';
var parentClass = (options && options.parentClass) || 'header';
var childClassPrefix = (options && options.childClassPrefix) || '';
var collapsedClass = (options && options.collapsedClass) || 'collapsed';
/**
* Recursively hide all child rows or show immediate children
*
* All direct children must have a class that is the same as the parent id
* with an optional prefix
*
* @param {object} parentRow row element object
*/
var toggleRowChildren = function(parentRow) {
/**
* Replicate jQuery toggle
*
* @param {object} row element object
*/
var toggle = function(row) {
row.style.display = row.style.display ? '' : 'none';
return row;
};
/**
* Encapsulate the recursion check
*
* @param {object} row element object
* @return {boolean} if the row is collapsible
*/
var collapsible = function(row) {
return row.classList.contains(parentClass) &&
!row.classList.contains(collapsedClass);
};
var childClass = childClassPrefix + parentRow.getAttribute('id');
var childrenRows = parentRow.parentNode.querySelectorAll('tr.'+childClass);
// toggle all the children
childrenRows.forEach(function(row){
toggle(row);
// if a child is a parent and isn't collapsed
if (collapsible(row)) {
// recurse to the child
toggleRowChildren(row);
}
});
// 'mark' that the child has been hidden or not
parentRow.classList.toggle(collapsedClass);
};
return {
init : function() {
/**
* event delegation on the table rather than on the rows
* @link https://davidwalsh.name/event-delegate
*/
document.querySelectorAll(tableSelector).forEach(function (table) {
table.addEventListener('click', function (elem) {
// click happens on the td/th element, need the parent
if (elem.target && elem.target.parentNode.matches('tr.'+parentClass)) {
toggleRowChildren(elem.target.parentNode);
}
});
});
}
};
};
if (typeof jQuery === 'undefined') throw "jQuery Required";
/**
* DOM ready execution
*
* @param {object} $
* @returns {undefined}
* @link http://api.jquery.com/jQuery/#jQuery3 failsafe $ alias
* @licence MIT
*/
jQuery(function ($) {
/**
* Hierarchical table rows object
*
* @type object
* @link http://www.dustindiaz.com/json-for-the-masses/ Functional vs Classy section
*/
var treeTable = {
parentClass : 'header',
childClassPrefix : '',
collapsedClass : 'collapsed',
/**
* Set up the event handler
*
* @param {string} parentClass e.g. 'parent'
* @param {string} collapsedClass e.g. 'hidden'
* @param {string} childClassPrefix e.g. 'parentId'
*/
init : function(parentClass, collapsedClass, childClassPrefix) {
if (parentClass !== undefined) {
this.parentClass = parentClass;
}
if (collapsedClass !== undefined) {
this.collapsedClass = collapsedClass;
}
if (childClassPrefix !== undefined) {
this.childClassPrefix = childClassPrefix;
}
/**
* event delegation on the table rather than on the rows
* @link http://24ways.org/2011/your-jquery-now-with-less-suck/ Delegation section
*/
$('table').on('click', 'tr.'+treeTable.parentClass, function () {
treeTable.toggleRowChildren($(this));
});
},
/**
* Recursively hide all child rows or show immediate children
*
* @param {object} parentRow jQuery row element object
*/
toggleRowChildren : function(parentRow) {
// all direct children must have a class that is the same as the parent id with an optional prefix
var childClass = this.childClassPrefix+parentRow.attr('id');
/**
* cache the children selection
* @link http://24ways.org/2011/your-jquery-now-with-less-suck/ Caching section
*
* use the parent row as 'selector context'
* @link http://api.jquery.com/jQuery/#selector-context
*
* use the faster element selector followed by a slower filter on the .childClass
* @link http://24ways.org/2011/your-jquery-now-with-less-suck/ Selector optimization section
*
*/
var childrenRows = $('tr', parentRow.parent()).filter('.'+childClass);
// toggle all the children
childrenRows.toggle();
// foreach of the children
childrenRows.each(function(){
// if a child is a parent and isn't collapsed
if ($(this).hasClass(treeTable.parentClass) && !$(this).hasClass(treeTable.collapsedClass)) {
// recurse to the child
treeTable.toggleRowChildren($(this));
}
});
// 'mark' that the child has been hidden or not
parentRow.toggleClass(this.collapsedClass);
}
};
// only this bit actually needs to wait for the DOM
treeTable.init();
});
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Toggle Row Children</title>
<style>
body {
color: #444;
font-size: 18px;
margin: 40px auto;
max-width: 720px;
line-height: 1.4;
font-family: sans-serif;
padding: 10px 16px;
}
h1,h2,h3 {
line-height: 1.2
}
a {
text-decoration: none;
}
h1,h2,h3,h4,h5,h6 {
font-weight: normal;
}
table {
border-spacing: 0;
border-collapse: collapse;
width: 100%;
}
th, td {
border-top: 1px solid #444;
padding: 12px;
text-align: left;
}
th:hover {
cursor: pointer;
}
tr.active {
background-color: #f1f1f1;
}
</style>
</head>
<body>
<div class="container">
<h1>Toggle Row Children</h1>
<p>Click on a header row to toggle</p>
<table>
<tr id="cat1" class="parent">
<th>Cat1</th><th>Row</th>
</tr>
<tr class="child-cat1">
<td>&nbsp;&nbsp; data1</td><td>data2</td>
</tr>
<tr id="cat1a" class="parent child-cat1">
<th>&nbsp;&nbsp; Cat1a</th><th>Row</th>
</tr>
<tr class="child-cat1a">
<td>&nbsp;&nbsp;&nbsp;&nbsp; data1</td><td>data2</td>
</tr>
<tr class="child-cat1a">
<td>&nbsp;&nbsp;&nbsp;&nbsp; data3</td><td>data4</td>
</tr>
<tr id="cat2" class="parent">
<th>Cat2</th><th>Row</th>
</tr>
<tr class="child-cat2">
<td>&nbsp;&nbsp; data1</td><td>data2</td>
</tr>
</table>
</div>
<script type="text/javascript" src="row_toggle.js"></script>
<script type="text/javascript">
(function () {
var myTreeTable = treeTable({
parentClass: 'parent',
collapsedClass: 'active',
childClassPrefix: 'child-'
});
// only this bit actually needs to wait for the DOM
document.addEventListener("DOMContentLoaded", myTreeTable.init.bind(myTreeTable));
})();
</script>
</body>
</html>
@ianchanning
Copy link
Author

Setup

This works for multiple levels of children. It relies on the parent row id being the immediate child row class. e.g.

  • <tr id="cat1" class="header"> (parent)
  • <tr id="cat1a" class="cat1 header"> (child & parent)
  • <tr class="cat1a"> (grandchild)

All parents need a 'parent' class, the default is "header".

and after clicking row id=cat1 you would have

  • <tr id="cat1" class="header collapsed"> (parent)
  • <tr id="cat1a" class="cat1 header collapsed" style="display:none;"> (child & parent)
  • <tr class="cat1a" style="display: none;"> (grandchild)

Options

The treeTable.init() has the following options: parentClass, collapsedClass, childClassPrefix, e.g. treeTable.init('parent', 'hideChildren', 'parentId') could have the following structure:

  • <tr id="1" class="parent"> (parent)
  • <tr id="2" class="parentId1 parent"> (child & parent)
  • <tr class="parentId2"> (grandchild)

and after clicking row id=1 you would have

  • <tr id="1" class="parent hideChildren"> (parent)
  • <tr id="2" class="parentId1 parent hideChildren" style="display:none;"> (child & parent)
  • <tr class="parentId2" style="display:none;"> (grandchild)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment