46

I have some data to display that is both tabular and hierarchical. I'd like to let the user be able to expand and collapse the nodes.

Sort of like this, except functional:

http://www.maxdesign.com.au/articles/tree-table/

What would be the best way to approach this? I'm not adverse to using an off-the-shelf plugin.

John Slegers
  • 45,213
  • 22
  • 199
  • 169
justkevin
  • 3,089
  • 3
  • 28
  • 33

6 Answers6

44

SlickGrid has this functionality, see the tree demo.

If you want to build your own, here is an example (jsFiddle demo): Build your table with a data-depth attribute to indicate the depth of the item in the tree (the levelX CSS classes are just for styling indentation): 

<table id="mytable">
    <tr data-depth="0" class="collapse level0">
        <td><span class="toggle collapse"></span>Item 1</td>
        <td>123</td>
    </tr>
    <tr data-depth="1" class="collapse level1">
        <td><span class="toggle"></span>Item 2</td>
        <td>123</td>
    </tr>
</table>

Then when a toggle link is clicked, use Javascript to hide all <tr> elements until a <tr> of equal or less depth is found (excluding those already collapsed):

$(function() {
    $('#mytable').on('click', '.toggle', function () {
        //Gets all <tr>'s  of greater depth below element in the table
        var findChildren = function (tr) {
            var depth = tr.data('depth');
            return tr.nextUntil($('tr').filter(function () {
                return $(this).data('depth') <= depth;
            }));
        };

        var el = $(this);
        var tr = el.closest('tr'); //Get <tr> parent of toggle button
        var children = findChildren(tr);

        //Remove already collapsed nodes from children so that we don't
        //make them visible. 
        //(Confused? Remove this code and close Item 2, close Item 1 
        //then open Item 1 again, then you will understand)
        var subnodes = children.filter('.expand');
        subnodes.each(function () {
            var subnode = $(this);
            var subnodeChildren = findChildren(subnode);
            children = children.not(subnodeChildren);
        });

        //Change icon and hide/show children
        if (tr.hasClass('collapse')) {
            tr.removeClass('collapse').addClass('expand');
            children.hide();
        } else {
            tr.removeClass('expand').addClass('collapse');
            children.show();
        }
        return children;
    });
});
Franklin Yu
  • 8,920
  • 6
  • 43
  • 57
bcoughlan
  • 25,987
  • 18
  • 90
  • 141
  • 1
    Very nice:-) Just a small correction: Comment should be: // Gets all 's of greater depth.. NOT // Gets all 's of greater or equal depth.. It is only the tr's with a grater depth that we want to hide. Equal depth we want to show:) The logic is still right. – Sindre Myren Feb 12 '13 at 10:11
  • 1
    this comment might get flagged, but i don't care, i want to thank you for that simple implementation of collapsible tree grid. you deserve a thousand upvote! – stevenferrer Oct 23 '18 at 08:25
  • Looks good! Can you tell how it can be initially started as everything collapsed? – user2988257 Feb 22 '21 at 15:07
38

In modern browsers, you need only very little to code to create a collapsible tree :

var tree = document.querySelectorAll('ul.tree a:not(:last-child)');
for(var i = 0; i < tree.length; i++){
    tree[i].addEventListener('click', function(e) {
        var parent = e.target.parentElement;
        var classList = parent.classList;
        if(classList.contains("open")) {
            classList.remove('open');
            var opensubs = parent.querySelectorAll(':scope .open');
            for(var i = 0; i < opensubs.length; i++){
                opensubs[i].classList.remove('open');
            }
        } else {
            classList.add('open');
        }
        e.preventDefault();
    });
}
body {
    font-family: Arial;
}

ul.tree li {
    list-style-type: none;
    position: relative;
}

ul.tree li ul {
    display: none;
}

ul.tree li.open > ul {
    display: block;
}

ul.tree li a {
    color: black;
    text-decoration: none;
}

ul.tree li a:before {
    height: 1em;
    padding:0 .1em;
    font-size: .8em;
    display: block;
    position: absolute;
    left: -1.3em;
    top: .2em;
}

ul.tree li > a:not(:last-child):before {
    content: '+';
}

ul.tree li.open > a:not(:last-child):before {
    content: '-';
}
<ul class="tree">
  <li><a href="#">Part 1</a>
    <ul>
      <li><a href="#">Item A</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item B</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item C</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item D</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item E</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
    </ul>
  </li>

  <li><a href="#">Part 2</a>
    <ul>
      <li><a href="#">Item A</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item B</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item C</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item D</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item E</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
    </ul>
  </li>

  <li><a href="#">Part 3</a>
    <ul>
      <li><a href="#">Item A</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item B</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item C</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item D</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
      <li><a href="#">Item E</a>
        <ul>
          <li><a href="#">Sub-item 1</a></li>
          <li><a href="#">Sub-item 2</a></li>
          <li><a href="#">Sub-item 3</a></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

(see also this Fiddle)

John Slegers
  • 45,213
  • 22
  • 199
  • 169
  • Thank you for your wonderful answer. I'm trying to implement your solution but it doesn't seem to work in my setup. I've posted a question about this. Please let me know what am I doing wrong? http://stackoverflow.com/questions/43016555/couldnt-apply-javascript-to-my-template-in-flask-how-do-i-resolve-that-in-the – आनंद Mar 25 '17 at 14:59
  • 1
    @AnandTyagi : Can you post your code on [**JSFiddle**](https://jsfiddle.net/), or somewhere else online? I'll need to be able to debug your code to figure out what's wrong with it! – John Slegers Mar 27 '17 at 08:06
  • Thanks for responding. The issue has been resolved by the good people of stackoverlow. – आनंद Mar 27 '17 at 11:10
  • 1
    I know this is old, but I found this to be the simplest and best answer. Works perfect in my project – lostInTheTetons Apr 14 '17 at 15:02
  • I think you might need to `e.preventDefault()` for it to work – ospider Jul 28 '18 at 00:54
  • @ospider : It works fine without `e.preventDefault()`. Still, it is indeed better to add it, so I updated my code. – John Slegers Jul 28 '18 at 18:10
  • @JohnSlegers href url is applied to + symbol also. When i try to click it goes to the href location. Href url must be only apply to
  • tag. Kindly help me to resolve this issue
  • – Ask Xah Jan 31 '19 at 10:22
  • @आनंद : Clicking the + symbol should open the submenu. It works fine in my browser. Could you please tell me what browser and operating system you are using? – John Slegers Feb 01 '19 at 11:12
  • 4
    This code **does not** take care of the tabular part of the data. – Ronan Paixão Oct 18 '19 at 19:04
  • @RonanPaixão : The tabular part? – John Slegers Oct 18 '19 at 23:52
  • 1
    @JohnSlegers Yes. The OP specifically asked about displaying data that is *"both tabular and hierarchical"*. Your example shows hierarchical data, but no tabular data, which should be displayed in columns to the right of the tree (see the OP's link for an example). – Ronan Paixão Oct 19 '19 at 19:20