Creating A Tree Widget Using Struts2
Struts 2 includes the dojo toolkit and provides a taglib for using dojo features in a java environment. The <s:tree> tag can be used to generate a tree widget which can be very useful for displaying hierarchical data. For example, I recently used it in a project to display the folder structure for a filesystem.
This is a screenshot of what the rendered tree looks like.

Our final tree will contain the following features
- Ability to set a default selected node
- Trapping node select events
- Trapping expand/collapse events
- Saving/restoring the state of the tree
If you are of the impatient type and want a quick solution you can jump to the summary with working code.
Struts2 contains the showcase web application which provides some sample code. It can be downloaded from struts2 downloads page. (Download the sample applications package).
Unfortunately, some features in the tree example are broken as of now. We can use the tree example as a reference and build upon it to add more features.
I am discussing the presentation logic in the jsp. If you want to understand how the data structure to be displayed is created have a look at the following class in the showcase application.
org.apache.struts2.showcase.ajax.tree.Category
Displaying the tree
To begin with our jsp must contain the <s:head> tag inside the <head> section. This tag tells struts to paint the initialization code for dojo.
<s:head theme="ajax" debug="true" />To paint the tree we use the <s:tree> tag
<div style="float:left; margin-right: 50px;"> <s:tree id="contentTree" name="contentTree" theme="ajax" rootNode="%{rootNode}" childCollectionProperty="children" nodeIdProperty="id" nodeTitleProperty="name" treeSelectedTopic="treeSelected"> </s:tree> </div>
The “rootNode” attribute defines the data structure(Collection object) that will be displayed in the form of a tree. This needs to be set via an action. In the showcase example the ShowDynamicTreeAction.java is doing this job for us.
Capturing node select events
In the <s:tree> tag above the “treeSelectedTopic” defines the dojo event topic that will be published when a node of the tree is selected by the user.
This however, does not work by default in struts 2.0.9. A jira ticket has been lodged for this https://issues.apache.org/struts/browse/WW-1813 and it will be fixed in version 2.1.0
To work around this for the current version download the updated version of the file tree.ftl from the SVN and place it in your WEB_APPLICATION_ROOT/templates/ajax directory
For trapping node select events make a javascript funtion that would be called when the event fires and connect it to the event by using dojo.topic.subscribe as follows.
function treeNodeSelected(message) { /* * Place your logic here * I am just displaying the id of the node that was clicked */ alert(message.node.widgetId); }; dojo.event.topic.subscribe("treeSelected",this,treeNodeSelected);
Trapping expand/collapse events
The TreeLoadingController widget is required for being able to capture expand and collapse events. We will also be using the tree controller extensions which will give us additional ability to control the loading of the tree.
First, tell dojo to load the required files
//Load the tree controller widget and controller extension dojo.require("dojo.widget.TreeLoadingController"); dojo.require("dojo.widget.TreeControllerExtension"); // Add extensions to controller. dojo.lang.mixin(dojo.widget.byId('treeController'), dojo.widget.TreeControllerExtension.prototype);
Also, add the tree controller widget to the body section of your page
<div dojoType="TreeLoadingController" widgetId="treeController" RPCUrl="local"></div>
Now subscribe to the expand and collapse events
dojo.event.topic.subscribe("contentTree/expand",saveExpandedIndices); dojo.event.topic.subscribe("contentTree/collapse",saveExpandedIndices);
“saveExpandedIndices” is the name of the function that I am going to use to save the state of the tree every time a node is expanded/collapsed. You can have your own function in its place if you want.
Saving the state of the tree
For saving the state of the tree. The treeController widget provides a function called “saveExpandedIndices” which returns a multi dimension array of integers, each integer representing an expanded node. You can save this array based on your logic. For my implementation I am storing it inside a cookie. For converting the object to a string I use json.js which you can get from json.org
function saveExpandedIndices(message) { indices = dojo.widget.byId('treeController').saveExpandedIndices( dojo.widget.byId('contentTree') ); if (readCookie("categoryTreeState")==null) createCookie("categoryTreeState",indices.toJSONString()); else updateCookie("categoryTreeState",indices.toJSONString()); }
We can load the same state of the tree anytime. I use it at the body onload event so that when the user revisits that page it is in the same state.
function restoreExpandedIndices(indices) { dojo.widget.byId('treeController').restoreExpandedIndices( dojo.widget.byId('contentTree'), indices ); } function bodyOnLoad(){ treeState = readCookie("categoryTreeState"); indices = treeState.parseJSON(); restoreExpandedIndices(indices); }
Setting the default selected element
Sometime you might want to select a node of the tree on page load or at any other event. This can be accomplished by the following code.
treeSelector = dojo.widget.byId('treeSelector_contentTree') //The id will be treeSelector_TREENAME (its defined in tree.ftl) try // try to select node based on ID { treeNode = dojo.widget.byId(ID) treeSelector.doSelect(treeNode); //publish treeSelected topic to trigger handling of event dojo.event.topic.publish("treeSelected", {node: dojo.widget.byId(treeSelection)}); } catch (e)// if unable to select or node not found { treeNode = tree.getDescendants()[1] // try selecting the root node treeSelector.doSelect(treeNode); dojo.event.topic.publish("treeSelected", {node: tree.getDescendants()[1]}); }
Summarizing
Here is a summary of the code snippets put together which you can directly use in your application.
<html> <head> <s:head theme="ajax" debug="true"/> <script language="Javascript" type="text/javascript" src="/scripts/json.js"></script> <script language="Javascript" type="text/javascript" src="/scripts/cookies.js"></script> <script language="javascript"> //The following extensions are required to save or restore state of a tree dojo.require("dojo.widget.TreeLoadingController"); dojo.require("dojo.widget.TreeControllerExtension"); //This function is called after dojo scripts are loaded dojo.addOnLoad(function() { dojo.lang.mixin(dojo.widget.byId('treeController'), dojo.widget.TreeControllerExtension.prototype); //The following code will make sure that the saveExpandedIndices function is called everytime a node is collapsed or expanded dojo.event.topic.subscribe("contentTree/expand",saveExpandedIndices); dojo.event.topic.subscribe("contentTree/collapse",saveExpandedIndices); }); //The following function saves the state of the tree function saveExpandedIndices(message) { indices = dojo.widget.byId('treeController').saveExpandedIndices( dojo.widget.byId('contentTree') ); if (readCookie("categoryTreeState")==null) createCookie("categoryTreeState",indices.toJSONString()); else updateCookie("categoryTreeState",indices.toJSONString()); } //The following function restores the state of the tree. This can probably be called body-onload //You will have to pass it the stored indices object though function restoreExpandedIndices(indices) { dojo.widget.byId('treeController').restoreExpandedIndices( dojo.widget.byId('contentTree'), indices ); } function bodyOnLoad(){ treeState = readCookie("categoryTreeState"); indices = treeState.parseJSON(); restoreExpandedIndices(indices); } </script> </head> <body onload="bodyOnLoad()"> <div dojoType="TreeLoadingController" widgetId="treeController" RPCUrl="local"></div> <s:tree id="contentTree" name="contentTree" theme = "ajax" rootNode="%{rootNode}" childCollectionProperty="children" nodeIdProperty="id" nodeTitleProperty="name"> </s:tree> </body>
I am working on a tree with lazy loading that will load partially at first and the rest will be loaded based on when the user expands a node. I will post the code when I get it working!
Follow Ups:
• Struts2 – Dojo Dynamic Tree : In this example I have attempted to create a tree which loads partially at first and rest is loaded as and when requested by the user.
hi, where can I get den cookie.js?
look here
http://www.quirksmode.org/js/cookies.html
Hello!
Nice site
Bye
I followed the instructions as given in the ” Capturing node select events” , i added the tree.ftl file but still i am not able to capture the node select event . Is there something else we need to do to capture node select event.I am using struts 2.0.11. Please reply quickly.
This post is a little old and refers to struts 2.0.9 If you are using 2.0.11 then this information is not going to be useful for you since 2.0.10 onwards dojo is not a part of struts but a separate plugin. So you need to look for “ajax plugin for struts”. Have a look at the following page
http://struts.apache.org/2.x/docs/ajax.html
Thanks a lot.
But i could not find the location from which i can download “ajax plugin for struts”. Please can you specify the location from which i can down load it for Struts 2.o.11.
I did a search and found that the dojo/ajax plugin for struts 2.0.11 has not been released yet and is under development. You will have to download the source code and compile it yourself. More information about the source is here
http://struts.apache.org/dev/builds.html
Do you have a more dynamic example? Rather than hard coding the menus?
Hi Sean
I didn’t get what you mean by hard coding a menu.
I do have another example at which loads tree nodes based on when the nodes are clicked
http://www.codepencil.com/index.php/struts2-dojo-dynamic-tree/
but I’m not sure if that’s what you are looking for.
A lot of this is new to me. I’m not a JS person (yet), and still just getting into some html.
In the Category class, you have the menu coded as static. I have created a map object which is read from a database table I created. What I’m running into is not being familiar with how the root of the tree would be displayed. I understand how it is loaded in the example you have – since there is only one, it is being handled via the #request…
My issue is, I have loaded a Map object with my tree info (sort of like Category class), and there are n root levels. I guess I’m not familiar enough to know if there is a simple way on the getTree call to setup all the root nodes. I can email my eclipse project for some hacking
to get tree events capturing issue, i replaced struts2.0.11.jar with struts2.1.x.jar, and also copied dojo pulg-in , but still my code is not capturing events. where i am missing?