Monday, December 8, 2014

Building dynamic responsive multi-level menus with plain HTML and OmniFaces

Recently, I had to create a responsive multi-level menu with JSF 2.2. Requirements: the menu should
  • be created with dynamic structure from backend
  • be responsive, i.e. desktop- and mobile-friendly
  • have submenu items with navigation links
  • support touch events
  • support keyboard accessibility
PrimeFaces' menus were not a choice. They can indeed be created programmatically by model, but
  • they are not really responsive
  • submenu items only collapse / expand the submenus and can not contain navigation links
  • ...
Well, why not to pick any jQuery based plugin for responsive multi-level menus? There are a lot of plugins. See Useful List of Responsive Navigation and Menu Patterns. I choosen FlexNav.

But how to output the dynamic menu structure? ui:repeat is not a choice here because the structure (nested sub menus, etc.) is not known a priori. Fortunately, there is OmniFaces with o:tree, which allows to have full control over the markup of a tree hierarchy by declaring the JSF components or HTML elements in the markup. o:tree does not render any HTML markup by itself. Exactly what I need!

I ended up with this XHTML fragment mixing o:treeNode, o:treeNodeItem, o:treeInsertChildren and HTML elements defined by the mentioned FlexNav menu:
<h:outputScript library="js" name="jquery.flexnav.js"/>
<h:outputStylesheet library="css" name="flexnav.css"/>

<ul id="mainnavi" class="flexnav" data-breakpoint="640" role="navigation">
    <o:tree value="#{mainNavigationBean.treeModel}" var="item">
        <o:treeNode level="0">
            <o:treeNodeItem>
                <li class="item">
                    <a href="#{item.href}" title="#{item.title}">#{item.text}</a>
                    <o:treeInsertChildren/>
                </li>
            </o:treeNodeItem>
        </o:treeNode>
        <o:treeNode>
            <ul>
                <o:treeNodeItem>
                    <li>
                        <a href="#{item.href}" title="#{item.title}">#{item.text}</a>
                        <o:treeInsertChildren/>
                    </li>
                </o:treeNodeItem>
            </ul>
        </o:treeNode>
    </o:tree>
</ul>

<h:outputScript id="mainnaviScript" target="body">
    $(document).ready(function () {
        $("#mainnavi").flexNav({'calcItemWidths': true});
    });
</h:outputScript>
The OmniFaces' TreeModel with menu items is created programmatically. The Java code looks like
public TreeModel<NavigationItemDTO> getTreeModel() {
    // get menu model from a remote service
    NavigationContainerDTO rootContainer = remoteService.fetchMainNavigation(...);

    TreeModel<NavigationItemDTO> treeModel = new ListTreeModel<>();
    buildTreeModel(treeModel, rootContainer.getNavItem());

    return treeModel;
}

private void buildTreeModel(TreeModel<NavigationItemDTO> treeModel, List<NavigationItemDTO> items) {
    for (NavigationItemDTO item : items) {
        buildTreeModel(treeModel.addChild(item), item.getNavItem());
    }
}
And the end result (desktop variant):


Note that submenus are clickable and can be expanded on mouseover.

You see, JSF is flexible and sometimes you don't need full-blown components. Have fun!