Search code examples
jsfprimefacesomnifacesprimefaces-extensionstreetable

Decouple navigation and selection with JSF tree-like widget


I'm looking for a good approach to realize a tree-like JSF widget with the following requirements:

  • means to expand and collapse tree branches
  • ajax navigation through clicking on a tree node
  • multi-selection of nodes / branches via tri-state checkboxes
  • the former three features must work independent from each other
  • the solution must be compatible with PrimeFaces
  • I don't want to fork the framework in terms of writing a custom component

What I've come across:

  • PrimeFaces p:tree and p:treeTable
    • built-in selection feature provides a nice implementation with tri-state checkboxes but is tightly coupled to clicking on a node, which makes it unusable in terms of navigation (the selection also changes)
    • alternatively a custom implementation of the checkbox-column must reinvent the whole tri-state checkbox logic even with pe:triStateCheckbox (PrimeFaces Extensions)
  • OmniFaces o:tree seems to offer a high level of customization, but also leaves a lot of needle crafting remaining

Any hints, experiences are welcome.


Solution

  • I ended up with a solution build on p:treeTable with selectionMode="checkbox" and p:commandLink for navigation.

    To disable the 'full row' mouse click trigger also causing selection changes the JavaScript has been adjusted like this (PrimeFaces 5.3):

    <script type="text/javascript">
        //<![CDATA[
            PrimeFaces.widget.TreeTable.prototype.bindSelectionEvents = function() {
                var $this = this,
                rowSelector = '> tr.ui-treetable-selectable-node';
    
                this.tbody.off('mouseover.treeTable mouseout.treeTable click.treeTable', rowSelector)
                            .on('mouseover.treeTable', rowSelector, null, function(e) {
                                var element = $(this);
                                if(!element.hasClass('ui-state-highlight')) {
                                    element.addClass('ui-state-hover');
    
                                    if($this.isCheckboxSelection() && !$this.cfg.nativeElements) {
                                        element.find('> td:first-child > div.ui-chkbox > div.ui-chkbox-box').addClass('ui-state-hover');
                                    }
                                }
                            })
                            .on('mouseout.treeTable', rowSelector, null, function(e) {
                                var element = $(this);
                                if(!element.hasClass('ui-state-highlight')) {
                                    element.removeClass('ui-state-hover');
    
                                    if($this.isCheckboxSelection() && !$this.cfg.nativeElements) {
                                        element.find('> td:first-child > div.ui-chkbox > div.ui-chkbox-box').removeClass('ui-state-hover');
                                    }
                                }
                            })
                            .on('click.treeTable', rowSelector, null, function(e) {
                                //$this.onRowClick(e, $(this));
                                e.preventDefault();
                            });
    
                if(this.isCheckboxSelection()) {
                   var checkboxSelector =  this.cfg.nativeElements ? '> tr.ui-treetable-selectable-node > td:first-child :checkbox':
                            '> tr.ui-treetable-selectable-node > td:first-child div.ui-chkbox-box';
    
                        this.tbody.off('click.treeTable-checkbox', checkboxSelector)
                              .on('click.treeTable-checkbox', checkboxSelector, null, function(e) {
                                  var node = $(this).closest('tr.ui-treetable-selectable-node');
                                  $this.toggleCheckboxNode(node);
                              });
    
    
                        //initial partial selected visuals
                        if(this.cfg.nativeElements) {
                            this.indeterminateNodes(this.tbody.children('tr.ui-treetable-partialselected'));
                        }
                }
            };
        //]]>
    </script>
    

    I also changed some CSS, mainly:

    .ui-treetable .ui-treetable-data tr.ui-state-highlight,
    .ui-treetable .ui-treetable-data tr.ui-state-hover {
        cursor: default;
    }