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>
 	</table>
      <span class="bottom"><span jwcid="@RenderBlock" block="ognl:components.tblPages"/></span>
 </span>
 <span jwcid="tblPages@Block"><span jwcid="condPages"><span jwcid="tablePages"/></span></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"/>
 </form>
 

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); 
body.addInitializationScript(script);

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.

Use ${variable} instead of Insert component in Tapestry 4

Not long ago, some ppl in the Tapestry mailing list were asking for a shorcut when
using the Insert component.
They prefered writing ${user.name} instead of <span jwcid="Insert" value="ognl:user.name"/>

This functionality can easily be added into Tapestry 4. It’s a matter of finding which class to extend and which configuration point to set. So, let’s begin with the java code.
In this case, we’re going to extend org.apache.tapestry.parse.TemplateParser, overriding the method public TemplateToken[] parse(char[] templateData,
ITemplateParserDelegate delegate,
Resource resourceLocation)

In that method, we’re going to convert templateData to a String and then replace
all occurances of ${something} into <span jwcid=”@Insert” value=”ognl:something”/> . As seen, we’ll be adding a default binding of ognl, unless there’s already a binding.
In this way, ${message:home} will simply become <span jwcid=”@Insert” value=”message:home”/>. After that, we’ll just delegate to the base class. So here’s some code:

EasyInsertTemplateParser.java

 package andyhot.tap4.parse;
 
 import org.apache.hivemind.Resource;
 import org.apache.tapestry.parse.ITemplateParserDelegate;
 import org.apache.tapestry.parse.TemplateParseException;
 import org.apache.tapestry.parse.TemplateParser;
 import org.apache.tapestry.parse.TemplateToken;
 
 /**
  * For replacing ${something} into
  * &lt;span jwcid="@Insert" value="ognl:something"/&gt;
  * @author andyhot
  */
 public class EasyInsertTemplateParser extends TemplateParser
 {
     public TemplateToken[] parse(char[] templateData,
                                  ITemplateParserDelegate delegate,
                                  Resource resourceLocation)
     throws TemplateParseException
     {
         String template = new String(templateData);
         int pos = 0;
         boolean changed = false;
         do
         {
             pos = template.indexOf("${", pos);
             if (pos >=0)
             {
                 int pos2=template.indexOf("}", pos);
                 if (pos2>=0)
                 {
                     changed = true;
                     String oldValue = template.substring(pos + 2, pos2);
                     String newValue = applyBinding(oldValue);
                     template = template.substring(0, pos)
                     	+ "&lt;span jwcid=\"@Insert\" value=\"" + newValue + "\"/>"
                     	+ template.substring(pos2 + 1);
                 }
             }
         }
         while (pos>=0);
         if (changed == true)
         {
             System.out.println("Expanded: " + resourceLocation.getPath());
             //System.out.println(template);
         }
         return super.parse(template.toCharArray(), delegate, resourceLocation);
     }
 
     private String applyBinding(String value)
     {
         int pos = value.indexOf(":");
         if (pos<0)
         {
             return "ognl:" + value;
         }
         else
         {
             for (int i = 0; i < pos; i++)
             {
                 char c = value.charAt(i);
                 if ((c>='a' && c<='z') || (c>='A' && c<='Z'))
                     continue;
                 return "ognl:" + value;
             }
             return value;
         }
     }
 }

hivemodule.xml

 <?xml version="1.0"?>
 
 <module id="EastInsert" version="1.0.0" package="andyhot.tap4">
 
   <service-point id="EasyTemplateParser" interface="org.apache.tapestry.parse.ITemplateParser">
 
     Parses a template into a series of tokens that are used to construct a component's body.
 
     <invoke-factory model="threaded">
       <construct class="andyhot.tap4.parse.EasyInsertTemplateParser">
         <set-object property="factory" value="instance:org.apache.tapestry.parse.TemplateTokenFactory"/>
       </construct>
     </invoke-factory>
 
   </service-point>
 
   <service-point id="EasyTemplateSource" interface="org.apache.tapestry.services.TemplateSource">
 
     A smart cache for localized templates for pages and components.  Retrieves a template on
     first reference, and returns it on subsequent references.
 
 
     <invoke-factory>
       <construct class="org.apache.tapestry.services.impl.TemplateSourceImpl">
         <set-service property="parser" service-id="EasyTemplateParser"/>
         <set-service property="delegate" service-id="tapestry.parse.TemplateSourceDelegate"/>
         <set-object property="contextRoot" value="infrastructure:contextRoot"/>
         <set-service property="componentSpecificationResolver" service-id="tapestry.page.ComponentSpecificationResolver"/>
         <set-object property="componentPropertySource"  value="infrastructure:componentPropertySource"/>
         <event-listener service-id="tapestry.ResetEventCoordinator"/>
       </construct>
     </invoke-factory>
 
   </service-point>
 
   <contribution configuration-id="tapestry.InfrastructureOverrides">
 	 <property name="templateSource" object="service:EasyTemplateSource" />
   </contribution>
 
 </module>
 

Tapestry’s Table (cont.)

In a recent blog I described how I customized Tapestry’s contrib:Table, by making the pages links appear both at the top and at the bottom of the table, and by adding an optional form inside the table, in order to allow filtering of the results.

That customization was achieved by creating a custom Table component (just copy/paste the default .jwc and .html and subclass – if needed – the org.apache.tapestry.contrib.table.components.Table class).

Well, I’ve now gone even further by also including a custom TablePages component. This new component is able to display the pages link in a much nicer way, and uses assets for the Next, Back, First, Last links. Finally, I’ve made my custom Table component include a Block at the top ( just after the filter form ), so that I can easily add things there without needing to create new version of this component.

When I get the time, I’ll include all these stuff into TapFX – http://tapfx.sourceforge.net .
Here’s a screenshot of what I’ve got so far: