Using the Woodstock Sortable Table
Tuesday, February 27, 2007 |At long last, the Woodstock component set is finally here. At IEC, we have been anxiously awaiting its release for quite some time now, as we’ve been hoping to make use of the sortable data table component it offers, which we have now done. Having done it, allow me to show off the component a bit, as well as explain what I had to do make things work. It’s not hard, necessarily, just different enough to give one pause at first glance.
Table of Contents
Setting up your environment
The first thing you will need to do, obviously, is add the required jars to your project:
-
dataprovider.jar
-
dojo-0.4.1-ajax.jar
-
jsf-extensions-common-0.1.jar
-
jsf-extensions-dynamic-faces-0.1.jar
-
json.jar
-
prototype-1.5.0.jar
-
webui-jsf-suntheme.jar
-
webui-jsf.jar
To make things look pretty (and who doesn’t), you will also need to configure the Woodstock theme servlet:
1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>ThemeServlet</servlet-name>
<servlet-class>com.sun.webui.theme.ThemeServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ThemeServlet</servlet-name>
<url-pattern>/theme/*</url-pattern>
</servlet-mapping>
With that done, you will need to declare the Woodstock namespace on your page:
1
2
3
4
5
6
7
8
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:risb="http://java.sun.com/jsf/ri/sandbox"
xmlns:w="http://www.sun.com/webui/webuijsf">
Of course, XML being what it is, xmlns:w="http://www.sun.com/webui/webuijsf"
will have to be repeated on each page that uses a Woodstock component. I should also note that, while I was using "webuijsf" as the Woodstock examples suggested, I have started using "w" as it’s much smaller (and have updated this entry to reflect that). Having done that, you will now need to change your Facelets template/template client (or JSP header include, etc), changing <head>
to <w:head>
. This is necessary, as this is how the various JavaScript and CSS files required by Woodstock are included.
1
2
3
<w:head>
<w:link url="/style.css"/>
</w:head>
Alternately, you can use the themeLinks component:
1
2
3
<head>
<w:themeLinks />
</head>
The Table
We’re now ready to put a table on the page. Here’s a snippet from the application in which I’ve implemented the table:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<w:table id="table" clearSortButton="true" sortPanelToggleButton="true"
title: "Pending Units" filterText="#{ewl.group.filter.filterText}" "
deselectMultipleButton="true" selectMultipleButton="false">
<f:facet name="actionsTop">
<f:subview id="actionsTop">
<h:selectOneMenu id="selectedEngineer" value="#{ewl.selectedEngineer}">
<risb:selectItems value="#{ewl.engineers}" itemValue="#{item.name}"
itemLabel="#{item.name}" />
</h:selectOneMenu>
<w:button id="assignEngineerAction" text="Assign Engineer"
action="#{ewl.assignEngineer}" />
<w:image icon="TABLE_ACTIONS_SEPARATOR" />
<w:button text="Refresh" action="#{ewl.refresh}" />
</f:subview>
</f:facet>
<f:facet name="filter">
<w:dropDown submitForm="true" id="filter"
action="#{ewl.group.filter.applyBasicFilter}"
items="#{ewl.group.filter.filterOptions}"
onChange="if (filterMenuChanged(this) == false) return false"
selected="#{ewl.group.filter.basicFilter}" />
</f:facet>
<f:facet name="filterPanel">
<f:subview id="filterPanel">
<h:inputHidden id="customFilter"
value="#{ewl.group.filter.customFilter}" />
<w:markup tag="div">
Show only units for the engineer:
<h:selectOneMenu id="engineerFilter">
<risb:selectItems value="#{ewl.engineers}" itemValue="#{item.name}"
itemLabel="#{item.name}" />
</h:selectOneMenu>
<w:button action="#{ewl.group.filter.applyCustomFilter}"
onClick="applyCustomFilter('engineerFilter');" mini="true" text="OK">
<f:setPropertyActionListener value="assignedTo"
target="#{ewl.group.filter.customFilterField}" />
</w:button>
</w:markup>
<!-- snip! -->
</f:subview>
</f:facet>
<w:tableRowGroup id="pendingRowGroup" sourceData="#{ewl.group.provider}"
sourceVar="unit" selected="#{ewl.group.select.selectedState}"
binding="#{ewl.group.tableRowGroup}">
<w:tableColumn align="center" selectId="selectReferenceId">
<w:checkbox id="selectReferenceId" selected="#{ewl.group.select.selected}"
selectedValue="#{ewl.group.select.selectedValue}"
onClick="setTimeout('clicked()', 0);" />
</w:tableColumn>
<w:tableColumn alignKey="assignedTo" headerText="Assigned To"
sort="assignedTo">
<w:staticText text="#{unit.value.assignedTo}" />
</w:tableColumn>
<!-- snip! -->
</w:tableRowGroup>
</w:table>
At first glance, that’s quite overwhelming, and I’ll be the first to admit that I don’t understand everything that’s going on there, but I’ll try convey what I do understand. :) For good or bad, this sample does both sorting and filtering. The properties on <w:table>
should be fairly self-explanatory. The actionsTop
facet allows me to insert arbitrary markup into the area by that name in the table header. In this example, it is through this area that I’m able to perform various actions against the selected rows in the table: assign an engineer or refresh the table (i.e., clear any filters and reload the data the database).
Displaying Data
Finally, we come to the heart of the table, the tableRowGroup
. This is the point at which I had to smile and nod, and just do what I was told. The TLD docs have this to say of this component:
The tableRowGroup component is used to define attributes for XHTML elements, which are used to display rows of data. You can specify multiple w:tableRowGroup tags to create groups of rows. Each group is visually separate from the other groups, but all rows of the table can be sorted and filtered at once, within their respective groups.
Note that we bind this component to a property on the managed bean. This is where things get really…interesting. If you were to look at the example source code or the TLD docs for the table
, you would find a number of helper classes, such as Group
, Filter
, and Select
. If you are like me, your first inclination is to skip using these classes, hoping to simplify things a bit. Don’t. In fact, I took these classes and tweaked them a bit to make them more generally usable and bundled them in a utility library that we can use. If you’d like to use these classes, the complete source can be downloaded here. You can browse the source to see what all Group
does, but one of its most important functions is to create the TableDataProvider
the component will need. The easiest way I have found, which you will see in the class, is to wrap a List
of my model objects in an ObjectListDataProvider
:
1
2
3
4
5
6
7
8
9
public Group(String sourceVar, Object[] array) {
this(sourceVar);
provider = new ObjectArrayDataProvider(array);
}
// Construct an instance using given List.
public Group(String sourceVar, List list) {
this(sourceVar);
provider = new ObjectListDataProvider(list);
}
Now that we’ve bound the data to the tableRowGroup
, we need to display the data on the page. In the example above, I have two columns: one has a checkbox for selecting a row, and the other shows the assigned engineer. Again, this is somewhat of a black box for me, but as best as I can make out, the "select" column has a selection ID that will be used by the table’s JavaScript to manage selected rows. Note the the value of the `selectId
matches the id of the checkbox
component. The checkbox
itself has few properties to note. The first is the selected
and selectedValue
attributes, which are bound to methods on the Select
object (owned by the Group
object) that determine whether or not a given row has been selected. The third property is the onClick
(note the case) property. The JavaScript referenced here is used to update the table to reflect the selected of the row associated with the checkbox (From the TLD Docs: "The JavaScript setTimeout function is used to ensure checkboxes are selected immediately, instead of waiting for the JavaScript function to complete."):
1
2
3
4
var tableId = "pendingUnits:table";
function clicked () {
document.getElementById(tableId).initAllRows();
}
Sorting Data
The next column in the table is a sortable column. While most of the markup here is straightforward, note the alignKey
and sort
properties. These columns indicate the field on which to sort when the user selects that column. I am uncertain as to whether or not they have to be the same, but I’ve always seen them that way, so that’s the pattern I’ve followed. It is also probably important to point out how data is retrieved from the DataProvider
. In the staticText
component, you’ll see the value is set to #{unit.value.assignedTo}
. The variable unit
is the sourceVar
defined in the table
setup, and value
is a method on the DataProvider
that returns (in our case) the object for the given row.
Filtering Data
Filtering is also enabled on our table. The filter
facet is where I am able to specify the filters I’d like to be able to apply to the table. Due to a JavaScript issue I have yet to track down (which may or may not be related to my nascent Facelets support), my implementation here is a bit different from the Woodstock examples. Here is the source for filterMenuChanged
:
1
2
3
4
5
6
7
8
function filterMenuChanged(cb) {
if (cb.value == "_customFilter") {
var ret = document.getElementById(tableId).filterMenuChanged();
return ret;
} else if (cb.value == "FILTER_SHOW_ALL") {
window.location.href=window.location.href;
}
}
It basically checks for the special option element Woodstock adds to determine if a custom filter is being requested (which causes the filter panel to be displayed), or if the "show all" option was selected, which will clear the filter. Note that this JavaScript is not optimal and has changed a fair amount as my understanding of the component has grown, and will likely do so again. Ideally, I’ll solve the JavaScript error that prompted this so that this can go away.
The next item of interest is the filterPanel
facet, which is display when the user selects the "Custom Filter" option. The markup here pretty simple, in that all I have are a number of custom filters (though I’ve shown only one) that are nothing more than a label, an appropriate UIInput
component, and a button. The only thing really noteworthy is the JavaScript used to apply the filter. Via EL, we’re taking the value entered or selected by the user, and setting that on a property on the Filter
class (which I added to the Sun-provided class to make things more reusable). Since every field on the form will get set on the managed bean referenced via its EL, we can’t have them all pointing at the same property. To solve this problem, I use some simple JavaScript to copy the value in which I’m interested to a hidden field, which is the only one assigned to the desired property. I also use a <f:setPropertyActionListener>
to set which field should be filtered:
1
2
3
4
5
<w:button action="#{ewl.group.filter.applyCustomFilter}" mini="true"
text="OK" onClick="applyCustomFilter('timePending');">
<f:setPropertyActionListener value="timePendingClass"
target="#{ewl.group.filter.customFilterField}"/>
</w:button>
The source for applyCustomFilter
is
1
2
3
4
function applyCustomFilter(source) {
document.getElementById('pendingUnits:table:filterPanel:customFilter').value =
document.getElementById('pendingUnits:table:filterPanel:' + source).value;
}
When the form submits, the appropriate properties on the Filter
object are set, and the filters are applied to the DataProvider
:
1
2
3
4
5
6
7
8
9
public void applyCustomFilter() {
basicFilter = Table.CUSTOM_FILTER_APPLIED; // Set filter menu option.
filterText = "Custom - " + customFilter;
// Filter rows that do not match custom filter.
CompareFilterCriteria criteria = new CompareFilterCriteria(
group.getProvider().getFieldKey(customFilterField), customFilter);
// Note: TableRowGroup ensures pagination is reset per UI guidelines.
group.getTableRowGroup().setFilterCriteria(new FilterCriteria[] {criteria});
}
What It Looks Like
Here is a screen shot from the application from which this table was taken. It shows the rows sorted by the "Assigned To" field, a row is selected, and the custom filter panel is displayed:
Closing
And that’s "all" there is to it. I’ve worked with (and on) a fair number of JSF components, but this is likely the coolest with which I’ve had personal experience. The "coolness" comes at a cost, though, in that the component can be difficult to grasp at first. Hopefully, this "little" will flatten the learning curve just a little bit. And while you’re playing with the table, be sure to check out some of the other Woodstock components. They did a great job.
As a side note, many thanks to Ken Paulsen (of JSFTemplating and GlassFish admin console fame) for answering all of my questions, regardless of how silly they seemed. My employer, IEC (namely, my boss Mitch, and not just because he reads this ;) ) deserves many thanks as well for giving me the time to add Facelets support, without which we couldn’t be using Woodstock.