Tapestry’s Table

The Table component of Tapestry, found in the contrib library is one of those components that save your day! It’s associated with a nice object model, and allows you full customization (in the Tapestry’s User mailing list you can even find how to make each table row render 2 rows – quite useful when displaying lots of columns ).

It’s also very easy to extend it. I had already created a version that displays the pages span both at the top and at the bottom of the table , which is quite handy if you want to allow tables with more than 20 rows. I’ve done this by using a template such as:

 <span jwcid="$content$">
 <span jwcid="tableView">
     <span class="top"><span jwcid="@RenderBlock" block="ognl:components.tblPages"/></span>    
 	<table jwcid="tableElement">
 		<tr><span jwcid="tableColumns"/></tr>
 		<tr jwcid="tableRows"><td jwcid="tableValues"/></tr>
      <span class="bottom"><span jwcid="@RenderBlock" block="ognl:components.tblPages"/></span>
 <span jwcid="tblPages@Block"><span jwcid="condPages"><span jwcid="tablePages"/></span></span>

Notice that I include the pages rendering in a Block and add a RenderBlock at the start and at the end of the table. Also, both RenderBlocks are included in a span with class=”top” (for the first one) and class=”bottom” (for the last one) so that I can style them differently.

I’ve recently went on and added filtering functionality, thus creating a FilteringTable component. At first, I added this form after the first RenderBlock:

 <form jwcid="frmFilter@Form" class="tableFilter">
     <input jwcid="txfFilter@TextField" value="ognl:filter"/>

and also defined a String filter property in the jwc file. Notice here that no listener is needed since updating the filter property is done automatically by the framework and we don’t need to do any special work.

So, when a page includes this component, it can issue ( in pageBeginRender ) something like

String filter = ((FilteringTable)getComponent("myTable")).getFilter();

to get the filter String and do whatever it wants with it.

I’ve also added one more feature which goes like this: when a user enters a filter and hits ENTER the table is redrawn showing only the filtered data, but what I wanted was to give the focus back to the TextField. And I wanted this to happen only when the user changes the filter, and not when the page is loaded, reloaded or displaying next-previous pages.

So, I had to find a way to differentiate these 2 cases. At first, I tried using a persistent property, use a listener for the form submit, and set it to true.
Then when the component renders and if the property is true, it would do something like:

String script = "document." + form.getName() + "." + textField.getName() + ".focus();"; 
Body body = Body.get(cycle); 

and then set the property back to false. This didn’t really work as is, since the persistent property cannot be changed from within the renderComponent method. I also tried the PageDetachedListener and implement the pageDetached(PageEvent event) but even though it was called and no exception were thrown, the property was somehow always remaining to true when set by the listener. I also knew of the method that forgets a persistent property and also of the other method that hints to the engine that it can forget the property when it finishes rendering, but I was not online and couldn’t search the wiki.

Then I came across another solution which does not require the definition of any additional property! I just noticed that when the page containing the component renders by itself (i.e. because of a PageLink or when going to the next-previous page ) the renderComponent method of my component is called and in this method, if you do not call super.renderComponent(writer, cycle); at the begining (i.e. by calling it at the very end) then a call to
textField.getName() returns null (which is correct since the textField contained in our component hasn’t yet rendered and thus hasn’t been given a name). On the other hand, after a form submit, this very method returns a not null String with the name of the field (the explanation for this probably has to do with the rendering and rewind lifecycle). So, I now simply check for null (in the renderComponent method) and if true, do nothing. If however I get a not null response, I add the script to give the focus to the textfield!

What I want to do next is enhance the tablePages component (included in Table) and apart from allowing navigation to the next pages, make it show how many results were found, how many pages exists, and which are the currently showing records.