<t:tree2/>

Description

A component that provides an HTML-based tree from data supplied by a backing bean. The tree is highly customizable and allows for fine-grained control over the appearance of each of the nodes depending on their type. Almost any type of JSF component (text, image, checkbox, etc.) can be rendered inside the nodes and there is an option for client-side or server-side toggling of the expand/collapse state.

Screen Shot

e.g. Not a Visual Component

API

e.g.

component-family

org.apache.myfaces.HtmlTree

renderer-type

org.apache.myfaces.Tree

component-class

org.apache.myfaces.custom.tree2.HtmlTree

renderer-class

org.apache.myfaces.custom.tree2.HtmlTreeRenderer

tag-class

org.apache.myfaces.custom.tree2.TreeTag

http://myfaces.apache.org/tomahawk/tree2.html

Usage

Syntax

<t:tree2 
        [tomahawk_tree2_attributes]
        [alt_location_attributes]
        [ui_component_attributes] >
   <f:facet name="...">
      ... nested tags...
   <f:facet name="...">
   <f:facet>
      ... nested tags...
   </f:facet>
   ...
</t:tree2>

Instructions

The Tree2 component renders a tree representation of your data with a HTML table. The tree is dynamic: nodes are expanded or collapsed when the user clicks them. The component supports both client side interaction through JavaScript, and server side interaction. In the latter case, each user click results in a request/response cycle, which re-renders the tree in the new view state.

Note that in the latter case only the visible (expanded) data is sent to the client. In the first case (client side tree), the entire tree is sent to the browser with every HTML response. Each node of the tree carries a sizable amount of HTML code with it (think 200 characters on average, depending on the amount of information you want to show in for a node), and this information is sent to the browser also for nodes that are not visible to the end user when the page is rendered, because one of their ancestor nodes is collapsed. If you have a tree, e.g., 4 levels deep, with 10 children on average for each node, you will transmit 10 + 102 + 103 + 104 = 11 110 nodes of 200 characters, or 2 222 000 characters, or roughly 2MB of data. This example should make clear that, although the client side tree gives a very good user experience, you could grow a bandwith problem very rapidly, so be aware. This tree is fit for small trees, or medium sized trees on an intranet or over a broadband connection. For large trees, or when you need to take into account low-bandwidth connections, you should use the server side tree.

You can choose between client-side or server-side interaction with the clientSideToggle attribute. <t:tree2 clientSideToggle="false" ... gives you the servers-side interaction. true (client-side interaction) is the default for this attribute.

Attributes

name

required

description

value

true

var

false

varNodeToggler

false

showNav

false

Show the "plus" and "minus" navigation icons (default is true.) Value is ignored if clientSideToggle is true.

showLines

false

Show the connecting lines (default is true.)

clientSideToggle

false

Perform client-side toggling of expand/collapse state via javascript (default is true.)

showRootNode

false

Include the root node when rendering the tree (default is true.)

preserveToggle

false

Preserve changes in client-side toggle information between requests (default is true.)

Configuration

Don't need any extra configuration.

Notes and Known issues

Some known issues like incompatibility to other frameworks/component libs

Examples

Live example see: http://www.irian.at/myfaces/home.jsf

Example with styles: http://www.j4fry.org/J4Fry_Quick_Setup_Tomcat_MyFaces_Hibernate/index.faces

FAQ

Additional Information

Backing Bean

The tree2 component needs a TreeModel from a backing bean to operate. Usually, you give the TreeModel to the component using a value binding, like this:

  <t:tree2 value="#{myHandler.treeModel}" ...

This requires a class MyHandler, of which myHandler is an instance (probably created as a managed bean). This class then offers, in the example, a method getTreeModel() that returns a tree model of your data.

public class MyHandler {

...

  public TreeModel getTreeModel() {
    ...
  }

...
}

The tree model is actually a wrapper around a tree of TreeNode instances.

TreeNode is an interface, where only 4 methods are relevant to the operation of the tree2: String getType(), boolean isLeaf(), List getChildren() and int getChildCount(). All the other methods are leftovers, and should be removed in some future version. They demand an unnecessary effort of the developer of the backing bean.

int getChildCount() returns the number of children of this node. This can easily be implemented as:

  ...

  public final int getChildCount() {
    return getChildren().size();
  }

  ...

The method appears in the interface to make lazy loading of the children possible. An implementor could write code that returns the number of children without actually instantiating objects for all children beforehand.

boolean isLeaf() returns true if this tree node has no children. Thus, a straightforward implementation can be:

  ...

  public final boolean isLeaf() {
    return getChildren().isEmpty();
  }

  ...

Whatever the implementation is you provide, at all times the condition getChildren().isEmpty() ==> isLeaf() should hold. The isLeaf() actually controls how the node is rendered: as a leaf (which cannot be expanded further) or not.

String getType() decides how to render this particular node. In the JSF page, there should be facets nested in the <t:tree2> tage. The facet whose name matches the result of getType() will be choosen to render this tree node. There should be defined a facet with a name that matches the getType() of the tree nodes for all tree nodes in the tree. If no matching facet can be found for a given tree node, an error occurs. This method should never return null.

List getChildren() should return a List whose elements are of type TreeNode too, and that represent the tree nodes that should be rendered as the children of this tree node. The list should not contain null. If the size of the list does not match getChildCount(), you will get an error. The children are rendered in the order in which they occur in the list.

Using with Facelets

When using tree2 with Facelets you must be careful using inline EL inside a facet. For example

<t:tree2 ...>
   <f:facet name="nodeType">
      #{node.description}
   </f:facet>
   <f:facet name="anotherNnodeType">
      <h:commandLink ... value="#{node.description}"/>
   </f:facet>
   ...
</t:tree2>

Because inline EL/text is transient, JSF prunes it from the tree after rendering, so when the link is clicked it wont be able to restore the nodes of the first type. You'll get an IllegalArgumentException: Unable to locate facet with the name: nodeType.

JSP

TODO

Changing the Content of the Tree

or: Lazy loading child nodes from backend when expanding a tree node

The mailing list has many questions and discussions for this task and i (Marcel - a JSF beginner) ended up with this workaround. If you have a better solution please update this text!

The problem with this workaround is that i needed to remove the '+' icon with

and make the folder icon clickable with

to receive the mouse click events in my java code. In my NavigationBacker.java:processAction(ActionEvent e) i load the children nodes with data from EJB3-persistency.

The bad thing is that the + icon is now invisible, but i couldn't find a way to get the event when somebody clicks on the + icon. The org.apache.myfaces.custom.tree2.HtmlTree.java seems to register _expandControl = new HtmlCommandLink(); to get internally the + clicks, but i haven't found a way to receive the clicks in my code.

For navigation i use the TreeNode.getIdentifier() (see  #{node.identifier} ) with entries like

which are the primary keys of the backend database tables (i didn't find a smarter solution for navigation yet but it works).

Here are the code snippets:

navigation.jsp

  ...
  <t:tree2 id="serverTree" value="#{navigationBacker.treeData}"
    var="node" varNodeToggler="t" clientSideToggle="false" showNav="false"
    showRootNode="false">
    <f:facet name="project-folder">
      <h:panelGroup>
        <h:commandLink action="#{t.toggleExpanded}" actionListener="#{navigationBacker.processAction}">
            <t:graphicImage value="/images/yellow-folder-open.png"
                rendered="#{t.nodeExpanded}" border="0" />
            <t:graphicImage value="/images/yellow-folder-closed.png"
                rendered="#{!t.nodeExpanded}" border="0" />
        </h:commandLink>
        <h:commandLink action="#{navigationBacker.toViewId}"
            styleClass="#{t.nodeSelected ? 'documentSelected':'document'}"
            actionListener="#{navigationBacker.nodeClicked}"
            value="#{node.description}" immediate="true">
            <f:param name="db_id" value="#{node.identifier}" />
            ...
        </h:commandLink>
        <h:outputText value=" (#{node.childCount})" styleClass="childCount"
            rendered="#{!empty node.children}" />
      </h:panelGroup>
    </f:facet>
    <f:facet name="person-folder">
      <h:panelGroup>
        ...
  ...

NavigationBacker.java

  ...
    /**
     * Intercept when a lead node is expanded and load additional data.
     * @param event
     * @throws AbortProcessingException
     */
    public void processAction(ActionEvent event) throws AbortProcessingException {
            System.out.println("Entering processAction()");
            UIComponent component = (UIComponent) event.getSource();
            while (!(component != null && component instanceof HtmlTree)) {
                    component = component.getParent();
            }
            if (component != null) {
                    HtmlTree tree = (HtmlTree) component;
                    TreeNodeBase node = (TreeNodeBase) tree.getNode();
                    if (!tree.isNodeExpanded() && node.getChildren().isEmpty()) {
                        // navigation-view:foo:serverTree:0:0:_id4
                        // tree.getNódeId() == "0:1:0"
                        // node.getIdentifier() == "car_id=7"
                        // node.getType() == "car-detail"
                        Map<String, String> map = splitKeyValues(node.getIdentifier()); // some helper to split "car_id=7" or "car_id=7&person_id=12"
                        this.car_id = map.get("car_id");
                        if (this.car_id != null) {
                            appendPersonsNodes(node); // see example below
                        }
                        this.person_id = map.get("person_id");
                        if (this.person_id != null) {
                            appendLicensesNodes(node); // not shown
                        }
                    }
            }
    }
  ...

  ...
    /** Add to tree navigation for current car_id its Person childs */
    private void appendPersonsNodes(TreeNodeBase carDetailNode) {
        VariableResolver resolver = FacesContext.getCurrentInstance()
        .getApplication().getVariableResolver();
        PersonsTable personsTable = (PersonsTable) resolver
                .resolveVariable(FacesContext.getCurrentInstance(),
                        "personsTable");
        List<Person> personsList = personsTable.getCarPersons();
        for (Person o : personsList) {
            List<TreeNodeBase> list = carDetailNode.getChildren();
            list.add(new TreeNodeBase("person-folder", o.getDescription(),
                            "person_id=" + o.getPersonId(), true));
        }
        System.out.println("NavigationBacker fetched " + personsList.size() + " Persons for carId=" + this.car_id);
    }
  ...

Here is a helper to find the f:param in a h:commandLink which is needed in many cases:

  ...
    /**
     * If the JSF h:commandLink component includes f:param children, those
     * name-value pairs are put into the request parameter map for later use by
     * the action handler. Unfortunately, the same isn't done for
     * h:commandButton. This is a workaround to let arguments be associated with
     * a button.
     *
     * Because action listeners are guaranteed to be executed before action
     * methods, an action listener can use this method to update any context the
     * action method might need.
     *
     * From http://cvs.sakaiproject.org/release/2.0.0/
     * sakai2/gradebook/tool/src/java/org/sakaiproject/tool/gradebook/jsf/FacesUtil.java
     * Educational Community License Version 1.0
     */
    public static final Map getEventParameterMap(FacesEvent event) {
        Map<String, String> parameterMap = new HashMap<String, String>();
        List children = event.getComponent().getChildren();
        for (Iterator iter = children.iterator(); iter.hasNext();) {
            Object next = iter.next();
            if (next instanceof UIParameter) {
                UIParameter param = (UIParameter) next;
                parameterMap.put(param.getName(), "" + param.getValue());
            }
        }
        //System.out.println("parameterMap=" + parameterMap);
        return parameterMap;
    }
  ...

Note that in the above example the backer beans are in the session scope, which is configured in WEB-INF/examples-config.xml.

You may, at a later development stage, miss the browser back button functionality. You press the 'BACK' button of the browser and the navigation of the previous page (coming from the browser cache) does not work, now <x:saveState> comes into the game.

Put your backer beans in the "request" scope and declare them with

   <x:saveState id="navigationBacker_Save" value="#{navigationBacker}" />
   <x:saveState id="stindexScrollerList_Save" value="#{stindexScrollerList}" />
   ...

in your jsp page, and all is well :-).

Note that the backer beans must be Serializable to use x:saveState.

See http://wiki.apache.org/myfaces/How_JSF_State_Management_Works

Alternative Tree2 Lazy Loading Method...by jtmille3

The differences between my implementation and the one above is that my lazy loading happens in an extended TreeNode. Whereas the implementation above performs the lazy loading in the backing bean. With my example I can also leave the navigation buttons on the tree and leave the tree pretty much as is. Except for the extended TreeNode most of my code is an adaptation of the Tree2 example from the Apache MyFaces project.

JSP Code Snippet The clientSideToggle must remain false for my example. The client side tree will retrieve all nodes and won't work for this specific example.

<t:tree2 id="serverTree" value="#{treeBacker.treeModel}" var="node" varNodeToggler="t" preserveToggle="false" clientSideToggle="false">
                <f:facet name="folder">
                        <h:panelGroup>
                                <t:graphicImage value="/images/yellow-folder-open.png" rendered="#{t.nodeExpanded}" border="0" />
                                <t:graphicImage value="/images/yellow-folder-closed.png" rendered="#{!t.nodeExpanded}" border="0" />
                                <h:outputText value="#{node.description}" styleClass="nodeFolder" />
                        </h:panelGroup>
                </f:facet>
                <f:facet name="document">
                        <h:panelGroup>
                                <h:commandLink immediate="true" styleClass="#{t.nodeSelected ? 'documentSelected':'document'}" action="#{treeBacker.selectedNode}" actionListener="#{t.setNodeSelected}">
                                        <t:graphicImage value="/images/document.png" border="0" />
                                        <h:outputText value="#{node.description}" />
                                        <f:param name="docNum" value="#{node.identifier}" />
                                </h:commandLink>
                        </h:panelGroup>
                </f:facet>
        </t:tree2>

TreeBacker.java - managed bean

public final class TreeBacker {

        private TreeModelBase _treeModel;

        private String selectedNode;

        public TreeBacker() {
             // Initialize the tree with the root node
                TreeNode treeData = new LazyTreeNode("folder", "1", "1", false);
                _treeModel = new TreeModelBase(treeData);
        }

        public TreeModel getTreeModel() {
                return _treeModel;
        }

        public void selectedNode() {
                this.selectedNode = this.getTreeModel().getNode().getDescription();
        }

        public String getSelectedNode() {
                return selectedNode;
        }
}

LazyTreeNode.java

public class LazyTreeNode extends TreeNodeBase {

        private static final long serialVersionUID = -6870203633601493362L;

        public LazyTreeNode() {
        }

        public LazyTreeNode(String type, String description, boolean leaf) {
                super(type, description, leaf);
        }

        public LazyTreeNode(String type, String description, String identifier, boolean leaf) {
                super(type, description, identifier, leaf);
        }

        public List getChildren() {
             // if there are no children, try and retrieve them
                if (super.getChildren().isEmpty()) {
                   // create dummy tree nodes for example
                   int id = Integer.parseInt(getIdentifier());
                        for(int i = 0; i < id; i++) {
                       super.getChildren().add(new LazyTreeNode("document","" + id, "" + id,false));
                   }
                }

                return super.getChildren();
        }
}

Controlling Which Nodes are Expanded from the Backing Bean

TODO

Showing the Same Tree in Different Pages

TODO

Invalid Bit Mask

If you are getting an exception "Invalid bit mask of ..." this means the renderer is encountering an unexpectd combination of events. One explanation is that you have a node indicating that is expanded but its a leaf. Make sure you are returning the correct values for the isLeaf method of the TreeNode interface and that you are not toggling the expanded state (in a custom TreeModel, TreeState class, etc.) in an inappropriate manner.

Tree2 Navigation

One possible use of tree2 is a navigation widget. If you use command links in your tree2 and provide action methods that cause navigation to jump to another view, the default behavior of the component will probably be undesirable. By default, the TreeState information (which stores the list of expanded nodes and the currently selected node) is stored in the component. When a new view is reached as the result of navigation, a new component tree is created and a new instance of tree2 is created along with an empty TreeState. This means that the nodes that you expanded before clicking the link will not show up as expanded.

Fortunately there is a relatively simple fix. Lets start with the problematic JSP that contains command links with action methods.

treeNav.jsp

<t:tree2 id="wrapTree"
         value="#{treeBacker.treeModel}"
         var="node"
         varNodeToggler="t"
         clientSideToggle="false"
         showNav="true"
         showRootNode="false">

    <f:facet name="category">
        <h:panelGrid id="a" columns="2" cellpadding="0" cellspacing="0">
            <t:graphicImage value="/images/yellow-folder-open.png" rendered="#{t.nodeExpanded}" border="0"/>
            <t:graphicImage value="/images/yellow-folder-closed.png" rendered="#{!t.nodeExpanded}" border="0"/>
            <h:outputText value="#{node.description}" styleClass="nodeFolder"/>
        </h:panelGrid>
    </f:facet>

    <f:facet name="item">
        <h:panelGroup>
            <h:commandLink immediate="true"
                           styleClass="#{t.nodeSelected ? 'documentSelected':'document'}"
                           action="#{treeBacker.nodeSelected}">
                <h:panelGrid id="b" columns="2" cellpadding="0" cellspacing="0">
                    <t:graphicImage value="/images/document.png" border="0"/>
                    <h:outputText value="#{node.description}"/>
                </h:panelGrid>
                <f:param name="docNum" value="#{node.identifier}"/>
            </h:commandLink>
        </h:panelGroup>
    </f:facet>
</t:tree2>

Now we need to configure a backing bean. Notice how we set it to session scope. Don't worry, we won't be storing copies of the entire data model in the session, just the tree state (which contains a simple map of expanded nodes and the currently selected node).

faces-config.xml

  ...

  <managed-bean>
    <managed-bean-name>treeBacker</managed-bean-name>
    <managed-bean-class>org.apache.myfaces.petstore.backing.TreeBacker</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
  </managed-bean>

  ...

The backing bean will maintain a copy of the TreeState. Note how we set the transient property to true. This tells the tree2 component not to use its copy of state information but to rely on the backing bean copy. To keep things simple we are loading static data from the application context. In a real world app you would probably use a strategy for loading data dynamically from the database.

TreeBacker.java

public class TreeBacker implements Serializable
{
    private TreeState treeState;

    public TreeBacker()
    {
        treeState = new TreeStateBase();
        treeState.setTransient(true);
    }

    public TreeModel getTreeModel()
    {
        ServletContext context = (ServletContext)FacesContext.getCurrentInstance().getExternalContext().getContext();
        TreeNode rootNode = (TreeNode)context.getAttribute(PetstoreConstants.PETSTORE_DATA);
        TreeModel treeModel = new TreeModelBase(rootNode);
        treeModel.setTreeState(treeState);

        return treeModel;
    }
}

Tree2 Lazy Loading -- number of children not known blog...by Andrew Robinson

Please see my blog posting for this discussion and code: http://andrewfacelets.blogspot.com/2006/06/myfaces-tree2-creating-lazy-loading.html

Using Tree2 in a Portal...by David Chandler

To run Tree2 with client-side expansion, you need JavaScript in the page <HEAD>. Normally, this gets added by the Tomahawk ExtensionsFilter. This doesn't work in a portal, however, because servlet filters don't run in a portal. There are some patches in MYFACES-434 (portlet filter) you may be able to use, but here's an easier workaround. I've used this successfully with Tomahawk 1.1.3 in both Jetspeed2 and Liferay.

First, use Tree2 with server-side expansion so as not to require JavaScript. The ExtensionsFilter is therefore needed only to serve up the image resources needed by Tree2, and image requests are handled through the Faces Servlet, not the portal, so the ExtensionsFilter will run as normal for these requests. However, Tomahawk 1.1.3 checks to see if the ExtensionsFilter has been configured, which fails in the portal context. Fortunately, you can disable the check with a web.xml context param.

So to summarize, you can use Tree2 1.1.3 in a portal without any of the MYFACES-434 patches if

  1. You use server-side toggle
  2. You configure ExtensionsFilter as normal for the Faces Servlet

  3. You disable the ExtensionsFilter configuration check as follows in web.xml:

    <context-param>
        <param-name>org.apache.myfaces.CHECK_EXTENSIONS_FILTER</param-name>
        <param-value>false</param-value>
    </context-param>

/dmc

If you are unable to set-up the ExtensionsFilter, you can use the imageLocation attribute (ex. imageLocation="images") to specify the directory where the images will be taken from.

Alternative way to use Tree2 in a Portal...by BenSmith

I was able to get tree2 (and the other MyFacesExtensionsFilter-dependent controls) working in a portlet by using the Portals Bridge Portlet Filter along with the Tomahawk Bridge filter and the Faces Response filter by Shinsuke Sugaya: http://palab.blogspot.com/2007/01/tomahawk-bridge-091-released.html

Maybe the Tomahawk Bridge and Faces Response filters will one day become part of official MyFaces...?

Notes

Errors

In the 1.0.9 version of MyFaces extensions, this author regularly got errors from tree2 after extensive use. When using the client-side interaction (JavaScript), the expansion state of the nodes of the tree is stored in a cookie, which is discarded when the "user session" ends. On some browsers, this could mean the cookie will only disappear when the browser is quit. When these errors started occuring, quitting and relaunching the browser solved the problem every time.

Tree2 (last edited 2010-01-29 01:04:19 by newacct)