Sunday, March 01, 2009

A jQuery tree table for Wicket


I needed to enhance my Wicket DataView, which uses an html table and a particular look and feel, to render as a treetable in certain cases. The Wicket built-in TreeTable does not meet my needs, because it renders table cells as divs. Also I like the idea of finding easy ways to integrate powerful javascript plugins with Wicket.

I found that what I wanted to do was indeed possible with just one line of javascript: $("#treetable").treeTable(). It doesn't get simpler than that.

I found two popular tree table jQuery plugins, namely treeTable and jQTreeTable. After some playing around, I decided that treeTable was simpler and met my needs. To set up my Wicket webapp to use it, I downloaded the .js file to src/main/webapp/js, the CSS file to to src/main/webapp/css, and the necessary images (which are a bit harder to get at than they should be): toggle-expand-dark.png, toggle-collapse-dark.png, toggle-collapse-light.png, toggle-expand-light.png to to src/main/webapp/images.

The sample code below is the simplest way I could think of to show how to marry the jQuery tree table to Wicket, namely using a Loop. There are many other ways to do it, including what I am using in production, namely the DataPanel-DataProvider-DataView pattern. The picture above is a screenshot of my sample code.

The code that actually renders the tree table is a small part of the whole (i.e. the Loop); the rest is there to build the model, and other supporting code. Pay closest attention to the header in the html code.

As of this writing, I'm using jQuery 1.2.6 and Wicket 1.4-rc2.

TreeTable.html


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<wicket:head>
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/jquery.treeTable.js"></script>
<link href="css/jquery.treeTable.css" rel="stylesheet" type="text/css" />
<script type="text/javascript">
$(document).ready(function() {
$("#treetable").treeTable();
});
</script>
</wicket:head>
<body>
<table id="treetable" border="1">
<tr>
<th>ID</th>
<th>Name</th>
<th>Description</th>
</tr>
<tr wicket:id="row">
<td wicket:id="id"></td>
<td wicket:id="name"></td>
<td wicket:id="description"></td>
</tr>
</table>
</body>
</html>

TreeTable.java


public class TreeTable extends WebPage {

public TreeTable() {
super();

// Create a TreeModel or any kind of hierarchical data model
final TreeModel treeModel = getTreeModel();
// It's convenient to have an ordered list of tree nodes to iterate over
final List<DefaultMutableTreeNode> treeAsList = treeToList(treeModel);
// This map lets a child node look up the html id of its rendered parent tag
final Map<DefaultMutableTreeNode, String> treeMap = new HashMap<DefaultMutableTreeNode, String>();

add(new Loop("row", treeAsList.size()) {
private static final long serialVersionUID = 3443157723178494017L;

@Override
protected void populateItem(LoopItem item) {
final int index = item.getIteration();
final DefaultMutableTreeNode node = treeAsList.get(index);
final MyModelObject model = (MyModelObject) node
.getUserObject();
// Table cells
item.add(new Label("id", model.id));
item.add(new Label("name", model.name));
item.add(new Label("description", model.description));
// Build the tree hierarchy
// The id is necessary for the parent-child relationship
item.setOutputMarkupId(true);
treeMap.put(node, item.getMarkupId());
final DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) node
.getParent();
if (parentNode != null) {
final String parentId = treeMap.get(parentNode);
item.add(new AttributeAppender("class", new Model<String>(
"child-of-" + parentId), " "));
}
}
});
}

private List<DefaultMutableTreeNode> treeToList(TreeModel treeModel) {
final List<DefaultMutableTreeNode> ret = new ArrayList<DefaultMutableTreeNode>();
final DefaultMutableTreeNode node = (DefaultMutableTreeNode) treeModel
.getRoot();
recurse(ret, node);
return ret;
}

private void recurse(List<DefaultMutableTreeNode> ret,
DefaultMutableTreeNode node) {
ret.add(node);
for (Enumeration<?> e = node.children(); e.hasMoreElements();) {
final DefaultMutableTreeNode child = (DefaultMutableTreeNode) e
.nextElement();
recurse(ret, child);
}
}
/** A sample tree model */
private TreeModel getTreeModel() {

final DefaultMutableTreeNode ROOT = new DefaultMutableTreeNode(
new MyModelObject("ROOT", "This is root",
"Root is the base of the tree"));

final DefaultMutableTreeNode A = new DefaultMutableTreeNode(
new MyModelObject("A", "This is A", "A is a child of ROOT"));
ROOT.add(A);
final DefaultMutableTreeNode B = new DefaultMutableTreeNode(
new MyModelObject("B", "This is B", "B is a child of ROOT"));
ROOT.add(B);
final DefaultMutableTreeNode AA = new DefaultMutableTreeNode(
new MyModelObject("AA", "This is AA", "AA is a child of A"));
A.add(AA);
final DefaultMutableTreeNode BB = new DefaultMutableTreeNode(
new MyModelObject("BB", "This is B", "BB is a child of B"));
B.add(BB);

final DefaultTreeModel tree = new DefaultTreeModel(ROOT);
return tree;
}
/** A sample model object for the tree nodes */
private class MyModelObject implements Serializable {
private static final long serialVersionUID = -3058898204144314012L;
private final String id, name, description;

public MyModelObject(String id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
}
}


To get the expand and collapse icons to render the way I preferred, I made a minor change to the javascript to make the margin and padding be 100% CSS-driven.

In jquery.treeTable.js, I changed the line:
cell.prepend('<span style="margin-left: -' + options.indent + 'px;
padding-left: ' + options.indent + 'px" class="expander"></span>');

To:
cell.prepend('<span class="expander"></span>');

And in jquery.treeTable.css, I added the last two lines (margin-left and padding-left) to ".treeTable tr td .expander":
.treeTable tr td .expander {
background-position: left center;
background-repeat: no-repeat;
cursor: pointer;
padding: 0;
zoom: 1; /* IE7 Hack */
margin-left: -3px;
padding-left: 15px;
}

No comments: