Building Advanced Components For Tapestry Web Applications
In this excerpt from the book Tapestry 5: Building Web Applications author Alexander Kolesnikov looks at building advanced components. Tapestry is an Apache open-source framework for creating dynamic, robust, highly scalable web applications in Java. The book is a step-by-step guide to Java Web development with the developer-friendly Apache Tapestry framework.
Advanced Components
We are already familiar with a significant number of Tapestry components, and
using them, we can build sophisticated and functionally rich interfaces. It so happens
however, that many web applications have certain patterns repeating on their pages.
For example, they have tables that display different data or they have forms that
accept user input and then put the submitted details into some kind of Java
Bean object.
Wouldn’t it be convenient to have such repeating patterns already implemented as
components, ready to be dropped onto a page? It certainly would, and the current
version of Tapestry 5 already comes with a few components of this kind. They are
great effort savers for us, and in this chapter we are going to introduce them and use
them in our Celebrity Collector application.
Following are some of the components, we’ll examine:
- The Grid component allows us to display different data in a fairly
sophisticated table. We are going to use it to display our collection
of celebrities. - The Bean EditForm component greatly simpli fies creating forms for accepting
user input. We shall use it for adding a new celebrity to our collection. - The DateField component provides an easy and attractive way to enter or
edit the date. - The FCKEditor component is a rich text editor, and it is as easy to
incorporate into a Tapestry 5 web application, just as a basic TextField is.
This is a third party component, and the main point here is to show that
using a library of custom components in a Tapestry 5 application requires no
extra effort. It is likely that a similar core component will appear in a future
version of the framework.
Grid Component
Previously, we were able to display our collection of celebrities with the help of
the Loop component. It wasn’t dif ficult, and in many cases, that will be exactly the solution you need for the task at hand. But, as the number of displayed items grow
(our collection grows) different problems may arise.
We might not want to display the whole collection on one page, so we’ll need some
kind of a pagination mechanism and some controls to enable navigation from page
to page. Also, it would be convenient to be able to sort celebrities by first name, last
name, occupation, and so on. All this can be achieved by adding more controls and
more code to finally achieve the result that we want, but a table with pagination and
sorted columns is a very common part of a user interface, and recreating it each time
wouldn’t be ef ficient.
Thankfully, the Grid component brings with it plenty of ready to use functionality,
and it is very easy to deal with. Open the ShowAll.tml template in an IDE of
your choice and remove the Loop component and all its content, together with the
surrounding table:
<table width=”100%”>
<tr t:type=”loop” t:source=”allCelebrities”
t:value=”celebrity”>
<td>
<a href=”#” t:type=”PageLink” t:page=”Details”
t:context=”celebrity.id”>
${celebrity.lastName}
</a>
</td>
<td>${celebrity.firstName}</td>
<td>
<t:output t:format=”dateFormat”
t:value=”celebrity.dateOfBirth”/>
</td>
<td>${celebrity.occupation}</td>
</tr>
</table>
In place of this code, add this line: <t:grid t:source=”allCelebrities”/>
Run the application, log in to be able to view the collection, and you should see the
following result:
Quite an impressive result for a single short line of code, isn’t it? Not only are our
celebrities now displayed in a neatly formatted table, but also, we can sort the
collection by clicking on the columns’ headers. Also note that occupation now has
only the first character capitalized—much better than the fully capitalized version we
had before.
Here, we see the results of some clever guesses on Tapestry’s side. The only required
parameter of the Grid component is source, the same as the required parameter of
the Loop component. Through this parameter, Grid receives a number of objects of
the same class. It takes the first object of this collection and finds out its properties. It
tries to create a column for each property, transforming the property’s name for the
column’s header (for example, lastName property name gives Last Name column header) and makes some additional sensible adjustments like changing the case of
the occupation property values in our example.
All this is quite impressive, but the table, as it is displayed now, has a number
of deficiencies:
- All celebrities are displayed on one page, while we wanted to see how pagination works. This is because the default number of records per page for
Grid component is 25—more than we have in our collection at the moment. - The last name of the celebrities does not provide a link to the Details page anymore.
- It doesn’t make sense to show the Id column.
- The order of the columns is wrong. It would be more sensible to have the Last Name in the first column, then First Name, and finally the Date of Birth.
By default, to de fine the display of the order of columns in the table,
Tapestry will use the order in which getter methods are de fined in the
displayed class. In the Celebrity class, the getFirstName method is
the first of the getters and so the First Name column will go first, and
so on.
There are also some other issues we might want to take care of, but let’s first deal
with these four.
Tweaking the Grid
First of all let’s change the number of records per page. Just add the following
parameter to the component’s declaration:<t:grid t:source=”allCelebrities” rowsPerPage=”5″/>
Run the application, and here is what you should see:
You can now easily page through the records using the attractive pager control that
appeared at the bottom of the table. If you would rather have the pager at the top,
add another parameter to the Grid declaration:
<t:grid t:source=”allCelebrities” rowsPerPage=”5″pagerPosition=”top”/>
You can even have two pagers, at the top and at the bottom, by specifying
pagerPosition=”both”, or no pagers at all (pagerPosition=”none”). In the latter
case however, you will have to provide some custom way of paging through records.
The next enhancement will be a link surrounding the celebrity’s last name and
linking to the Details page. We’ll be adding an ActionLink and will need to know
which Celebrity to link to, so we have the Grid store using the row parameter. This
is how the Grid declaration will look
<t:grid t:source=”allCelebrities” rowsPerPage=”5″
row=”celebrity”/>
As for the page class, we already have the celebrity property in it. It should have
been left from our experiments with the Loop component. It will also be used in
exactly the same way as with Loop, while iterating through the objects provided by
its source parameter, Grid will assign the object that is used to display the current
row to the celebrity property.
The next thing to do is to tell Tapestry that when it comes to the contents of the Last
Name column, we do not want Grid to display it in a default way. Instead, we shall
provide our own way of displaying the cells of the table that contain the last name.
Here is how we do this:
<t:grid t:source=”allCelebrities” rowsPerPage=”5″
row=”celebrity”>
<t:parameter name=”lastNameCell”>
<t:pagelink t:page=”details” t:context=”celebrity.id”>
${celebrity.lastName}
</t:pagelink>
</t:parameter>
</t:grid>
Here, the Grid component contains a special Tapestry element <t:parameter>,
similar to the one that we used in the previous chapter, inside the If component. As
before, it serves to provide an alternative content to display, in this case, the content
which will fill in the cells of the Last Name column. How does Tapestry know this?
By the name of the element, lastNameCell. The first part of this name, lastName, is
the name of one of the properties of the displayed objects. The last part, Cell, tells
Tapestry that it is about the content of the table cells displaying the speci fied property.
Page 1 Of 6 –
Advanced Components from the book “Tapestry 5 – Building Web Applications”
Finally, inside <t:parameter>, you can see an expansion displaying the name of the
current celebrity and surrounding it with the PageLink component that has for its
context the ID of the current celebrity.
Run the application, and you should see that we have achieved what we wanted:
Click on the last name of a celebrity, and you should see the Details page with the
appropriate details on it.
All that is left now is to remove the unwanted Id column and to change the order of
the remaining columns. For this, we’ll use two properties of the Grid—remove and
reorder. Modify the component’s de finition in the page template to look like this:
<t:grid t:source=”celebritySource” rowsPerPage=”5″
row=”celebrity”
remove=”id”
reorder=”lastName,firstName,occupation,dateOfBirth”>
<t:parameter name=”lastNameCell”>
<t:pagelink t:page=”details” t:context=”celebrity.id”>
${celebrity.lastName}
</t:pagelink>
</t:parameter>
</t:grid>
Please note that re-ordering doesn’t delete columns. If you omit some
columns while specifying their order, they will simply end up last
in the table.
Now, if you run the application, you should see that the table with a collection of
celebrities is displayed exactly as we wanted:
Changing the Column Titles
Column titles are currently generated by Tapestry automatically. What if we
want to have different titles? Say we want to have the title, Birth Date, instead of
Date Of Birth.
The easiest and the most ef ficient way to do this is to use the message catalog, the
same one that we used while working with the Select component in the previous
chapter. Add the following line to the app.properties file:
dateOfBirth-label=Birth Date
Run the application, and you will see that the column title has changed
appropriately. This way, appending -label to the name of the property displayed
by the column, you can create the key for a message catalog entry, and thus change
the title of any column.
Right now, we are using the root message catalog, which is common for
all the pages of the application. Later, in Chapter 7, you will see how to
create a message catalog for every page.
Now you should be able to adjust the Grid component to most of the possible
requirements and to display with its help many different kinds of objects. However,
one scenario can still raise a problem.
Add an output statement to the getAllCelebrities method in the ShowAll page
class, like this:
public List<Celebrity> getAllCelebrities()
{
System.out.println(“Getting all celebrities…”);
return dataSource.getAllCelebrities();
}
The purpose of this is simply to be aware when the method is called. Run the
application, log in, and as soon as the table with celebrities is shown, you will see the
output, as : Getting all celebrities…
The Grid component has the allCelebrities property de fined as its source, so
it invokes the getAllCelebrities method to obtain the content to display. Note
however that Grid, after invoking this method, receives a list containing all 15
celebrities in collection, but displays only the first five.
Click on the pager to view the second page—the same output will appear again.
Grid requested for the whole collection again, and this time displayed only the
second portion of five celebrities from it. Whenever we view another page, the whole
collection is requested from the data source, but only one page of data is displayed.
This is not too ef ficient but works for our purpose.
Imagine, however, that our collection contains as many as 10,000 celebrities, and it’s
stored in a remote database. Requesting for the whole collection would put a lot of
strain on our resources, especially if we are going to have 2,000 pages.
We need to have the ability to request the celebrities, page-by-page—only the first
five for the first page, only the second five for the second page and so on. This ability
is supported by Tapestry. All we need to do is to provide an implementation of the
GridDataSource interface.
Here is a somewhat simpli fied example of such an implementation.
Using GridDataSource
First of all, let’s modify the IDataSource interface, adding to it a method for
returning a selected range of celebrities:
public interface IDataSource
{
List<Celebrity> getAllCelebrities();
Celebrity getCelebrityById(long id);
void addCelebrity(Celebrity c);
List<Celebrity> getRange(int indexFrom, int indexTo);
}
Next, we need to implement this method in the available implementation of this
interface. Add the following method to the MockDataSource class:
public List<Celebrity> getRange(int indexFrom, int indexTo)
{
List<Celebrity> result = new ArrayList<Celebrity>();
for (int i = indexFrom; i <= indexTo; i++)
{
result.add(celebrities.get(i));
}
return result;
}
Page 2 Of 6 –
Advanced Components from the book “Tapestry 5 – Building Web Applications”
The code is quite simple, we are returning a subset of the existing collection starting
from one index value and ending with the other. In a real-life implementation, we
would probably check whether indexTo is bigger than indexFrom, but here, let’s
keep things simple.
Here is one possible implementation of GridDataSource. There are plenty of output
statements in it that do not do anything very useful, but they will allow us to witness
the inner life of Grid and GridDataSet in tandem. Have a look at the code, and then
we’ll walk through it step-by-step:
package com.packtpub.celebrities.util;
import com.packtpub.celebrities.data.IDataSource;
import com.packtpub.celebrities.model.Celebrity;
import java.util.List;
import org.apache.tapestry.beaneditor.PropertyModel;
import org.apache.tapestry.grid.GridDataSource;
public class CelebritySource implements GridDataSource
{
private IDataSource dataSource;
private List<Celebrity> selection;
private int indexFrom;
public CelebritySource(IDataSource ds)
{
this.dataSource = ds;
}
public int getAvailableRows()
{
return dataSource.getAllCelebrities().size();
}
public void prepare(int indexFrom, int indexTo,
PropertyModel propertyModel, boolean ascending)
{
System.out.println(“Preparing selection.”);
System.out.println(“Index from ” + indexFrom + ” to ” + indexTo);
String propertyName = propertyModel == null?
null : propertyModel.getPropertyName();
System.out.println(“Property name is: ” + propertyName);
System.out.println(“Sorting order ascending: ” + ascending);
selection = dataSource.getRange(indexFrom, indexTo);
this.indexFrom = indexFrom;
}
public Object getRowValue(int i)
{
System.out.println(“Getting value for row ” + i);
return selection.get(i – this.indexFrom);
}
public Class getRowType()
{
return Celebrity.class;
}
}
First of all, when creating an instance of CelebritySource, we are passing an
implementation of IDataSource that will imitate an actual data source to its
constructor. In real life this could be some Data Access Object.
The GridDataSource interface that we implemented contains four methods:
getAvailableRows(), prepare(),getRowValue(), and getRowType(). The
simplest of them is getRowType(). It simply reports which type of objects are served
by this implementation of GridDataSource.
The getAvailableRows method returns the total number of entities available in the
data source (this is needed to know the number of pages and to construct the pager
properly). In our case, we are simply returning the size of a collection. In real life, this
method could contain a request to the database that would return the total available
number of records in a search result, without actually returning all those records.
If you insert an output statement into this method, you will notice that it is invoked
by Grid several times, even while a single page of the table is displayed. You will not
want to call the database that many times, so you will need to include some logic to
cache the result returned by this method, and update it only when necessary. But,
again, we are looking at the principles here, so let’s keep everything simple.
The prepare method does the main job of requesting the database and obtaining
a subset of entities from it to be displayed by the current page of the table.
The
subset is limited by the first two parameters—indexFrom and indexTo, which are
the indexes of the first and the last entities to be returned. They might be used in a
SELECT statement which would command the database to select all the entities and
then limit the selection in one way or another, depending on the SQL dialect.
The third parameter of this method, propertyModel, is used to de fine the column by
which the result should be sorted.
Again, we could use this parameter in a SELECTstatement, but here we are simply outputting the name of the property to see what
the Grid has passed to the method.
Finally, the ascending parameter could be used to de fine the order in which the
results should be sorted when speaking to the database, but we are just outputting
its value.
The last of the four methods, getRowValue(), returns the entity requested by Grid
using its index as a parameter. You will see how all this works soon.
To make use of the created CelebritySource, add the following method to the
ShowAll page class:
public GridDataSource getCelebritySource()
{
return new CelebritySource(dataSource);
}
Then change the source parameter of the Grid component in ShowAll.tml template:
<t:grid t:source=”celebritySource” rowsPerPage=”5″
row=”celebrity” t:model=”model”>
Run the application. Log in to view the ShowAll page, and as soon as the table with
celebrities is displayed, you should see the following output:
Preparing selection.
Index from 0 to 4
Property name is: null
Sorting order ascending: true
Getting value for row 0
Getting value for row 1
Getting value for row 2
Getting value for row 3
Getting value for row 4
From this you can see that to display the first page of results, the Grid component
invoked the methods of the GridDataSource implementation provided by its source
parameter in a certain succession. The output shows that the prepare method was
invoked with the indexFrom parameter set to 0, and the indexTo parameter set to
4. These are indexes of the first five celebrities in collection. The propertyModel
parameter was null, so no speci fic sorting was requested. Finally, the getRowValue
method was invoked five times to obtain an object to be displayed by each of the five
rows in the table.
Click on the pager to view the second page of results and the result will be similar,
only the indices will be different:
Preparing selection.
Index from 5 to 9
Property name is: null
Sorting order ascending: true
Getting value for row 5
Getting value for row 6
Getting value for row 7
Getting value for row 8
Getting value for row 9
Click on the header of one of the columns, and you will see the change in the
property name passed to the prepare method:
Property name is: lastName
Sorting order ascending: true
Now the data source will be requested to sort the result by last name. Of course, no
sorting will take place in our simpli fied example as we are simply outputting the
name of the property and not using it in an actual request to a database.
Click on the same column once again, and this time you will see that the order of
sorting is changed:
Property name is: lastName
Sorting order ascending: false
You can see from this example that Tapestry allows us to de fine precisely how
a database (or other data source) should be called, and we can request data,
page-by-page by creating an implementation of GridDataSource interface. The
Grid component will then invoke the methods of this interface and display the
information returned by them appropriately.
Next, we are going to see another advanced component, BeanEditForm. It is
somewhat similar to Grid in that it also can make use of BeanModel, and its
con figuration is pretty similar too.
BeanEditForm Component
Our current collection of celebrities is tiny, and it would be a good idea to provide
in the application functionality for adding new celebrities. Let’s begin by adding a template and a page class for a new page named AddCelebrity. Add to the page
class a single persistent property named celebrity, so that its code looks like this:
package com.packtpub.celebrities.pages;
import com.packtpub.celebrities.model.Celebrity;
import org.apache.tapestry.annotations.Persist;
public class AddCelebrity
{
@Persist
private Celebrity celebrity;
public Celebrity getCelebrity()
{
return celebrity;
}
public void setCelebrity(Celebrity celebrity)
{
this.celebrity = celebrity;
}
}
In the page template, declare one single component of type BeanEditForm and let its
id be the same as the name of the property of the page class, in our case, celebrity:
<html xmlns:t=”http://tapestry.apache.org/schema/tapestry_5_0_0.xsd”>
<head>
<title>Celebrity Collector: Adding New
Celebrity</title>
</head>
<body>
<h1>Adding New Celebrity</h1>
<t:beaneditform t:id=”celebrity”/>
</body>
</html>
We also need to somehow connect the new page to the rest of the application. For
instance, we could add this new PageLink component somewhere at the bottom of
the ShowAll page:
<a href=”#” t:type=”PageLink” t:page=”AddCelebrity”>Add new
Celebrity
</a><br/>
<a href=”#” t:type=”PageLink” t:page=”Start”>
Back to the Start Page
</a>
Page 3 Of 6 –
Advanced Components from the book “Tapestry 5 – Building Web Applications”
Finally, to make things more interesting, add another couple of properties to the
Celebrity class (don’t forget to generate getters and setters for them):
private String biography;
private boolean birthDateVerified;
set to true whenever we verify in some way that the birth date is correct.
Run the application, log in, and at the ShowAll page, click on the link leading to the
new AddCelebrity page. You will see the BeanEditForm in all its glory:
Isn’t it amazing how much can be done for us by Tapestry when we just drop one
component onto the page, with virtually no
configuration? Let’s see how all this
magic works:
- Since we didn’t specify any object parameter for BeanEditForm, Tapestry
decided that the name of the property should be the same as the id of the
BeanEditForm component. - We didn’t initialize the celebrity property, so its value is null, and still
everything works fine since BeanEditForm can create an instance of the
edited property as required. One consequence of this is that the type of
property should be a concrete class, not an interface. - BeanEditForm took all the properties of the edited class and created a field in
the form for each of them.
For each property that it can edit, BeanEditForm automatically selects a
certain control. For a string or a numeric property it displays a text box, for
an enumeration—a drop-down list, for a boolean property—a checkbox, for a
date—a DateField component (which will be described soon). However, we
can easily override the default choice if needed. - BeanEditForm generates a label for each property based on the property
name in the same way as the Grid component did. And in the same way
we can override the default label by providing an alternative for it in the
application’s message catalog, with a key like the propertyName label. - If the object edited by BeanEditForm, as provided by the page class, contains
some values in it, those values will be displayed in appropriate fields of the
form. As soon as you click on the Create/Update button, the values in the
form fields will be put into the appropriate properties of the edited object.
This list of features already looks quite impressive for a default con figuration, but
there are more miracles to see. Purely for the purpose of demonstration, enter some
non-numeric value, like abc, into the Id field and click on the Create/Update button.
You will see something similar to this:
Which means that in addition to everything else, BeanEditForm comes with a
pre-con figured system of user input validation, and applies reasonable restrictions to
its fields, like it prevents entering a non-integer value for an integer property.
User input validation is the topic for the next chapter, but you can already see that
without any efforts from our side, the validation system of Tapestry 5 does quite a
lot—it changes the style of the field in error, its label and adds an error marker, and
also displays an appropriate error message at the top of the form. In Chapter 7 you
will see that it can even display error messages in many different languages! Well,
the error message is somewhat misplaced at the moment, but we’ll deal with this
problem later.
Do you still remember that to obtain all this wealth of functionality, all we had to do
is to insert a short line of markup into the page template? Here it is again:
<t:beaneditform t:id=”celebrity”/>
Tweaking BeanEditForm
There are a few parameters that we could use to tweak the component. First of all,
you will probably want the submit button to display a different label, not the default
Create/Update. Nothing could be easier: <t:beaneditform t:id=”celebrity” t:submitLabel=”Save”/>You can also explicitly specify the object that BeanEditForm should work with, and
use an arbitrary id:<t:beaneditform t:id=”celebrityEditor” t:object=”celebrity”
t:submitLabel=”Save”/>
Although BeanEditForm made a lot of clever guesses, in many cases we shall want
to somehow infl uence the way it works. As with the Grid component in the previous
section, we’ll want to remove the Id field and change the order of fields in the form,
so that the Birth Date Veri fied check box is underneath the Birth Date control.
By the way, did you notice that the label for this control is Birth Date, not
Date Of Birth, as would be automatically generated by Tapestry? This is
because of the entry that we’ve added to the app.properties file.
That file is used by the whole application, and every label associated
with the dateOfBirth ID will automatically receive the value from the
message catalog.
The way we tidy up the BeanEditForm is very similar to what we did with the
Grid component:
<t:beaneditform t:id=”celebrity” t:submitLabel=”Save”
remove=”id”
reorder=”firstName,lastName,dateOfBirth,birthDateVerified,
occupation,biography”/>
The other change we might want to make is to change the control that is used for
Biography. Even though the biography will be brief, a text box is not convenient for
entering a long string. There is a much more convenient control for this purpose in
HTML, <textarea>. In Tapestry, such control can be displayed by the TextArea
component. Here is what we should do to override the default choice for editing the
biography property of the displayed object:
<t:beaneditform t:id=”celebrity” t:submitLabel=”Save” remove=”id”
reorder=”firstName,lastName,dateOfBirth,birthDateVerified,
occupation,biography”>
<t:parameter name=”biography”>
<t:label for=”biography”/>
<t:textarea t:id=”biography”
t:value=”celebrity.biography”/>
</t:parameter>
</t:beaneditform>
In a way similar to what we did with the Grid component to override the default
rendering of a certain column, we are using a <t:parameter> element. Here it
repeats the name of the property for which we want to provide an alternative editor.
Inside this element we are using a TextArea component, in the same way as we used
TextField in the previous chapter.
If you run the application now, the form should look like this:
This is already better. If you think that you’d prefer to have more space for a
biography, try this:
<t:textarea t:id=”biography” t:value=”celebrity.biography”
cols=”30″ rows=”5″/>
As cols and rows attributes do not belong to parameters of Tapestry’s TextArea
component, they will be simply passed to the resulting <textarea> HTML control.
Run the application and see how the form looks now.
At this point, let’s distract ourselves to explore the new component that magically
appeared in BeanEditForm, it deserves it.
DateField Component
This is a new addition that appeared only in the latest 5.0.6 version of Tapestry. Now
we can use this beautiful, JavaScript-powered control without seeing even a single
line of JavaScript.
DateField is based on an open source DHTML/JavaScript calendar that
can be found at http://www.dynarch.com/projects/calendar/.
Let’s add one more piece of information to those that we already collect from the
users at the Registration page—Date Of Birth. Add this table row to the template,
perhaps straight under the controls used for gender selection:
<tr>
<td>Gender:</td>
<td>
<t:radiogroup t:value=”gender”>
<input type=”radio” t:type=”radio” t:value=”male”/>
Male
<input type=”radio” t:type=”radio” t:value=”female”/>
Female
</t:radiogroup>
</td>
</tr>
<tr>
<td>Birth Date:</td>
<td>
<input type=”text” t:type=”datefield”
t:value=”dateOfBirth”/>
</td>
</tr>
We’ll also need a property to store the selected date in the Registration page class:
@Persist
private Date dateOfBirth;
public Date getDateOfBirth()
{
return dateOfBirth;
}
public void setDateOfBirth(Date dateOfBirth)
{
this.dateOfBirth = dateOfBirth;
}
Run the application, go to the Registration page, and you will see the new control on it:
Click on the small icon to the right, and in the beautiful calendar that opens you will
be able to choose a date:
However, by default the selected date will be displayed in the American format,
like 10/31/07 for the 31st of October. What if you would rather prefer to see it in
the European format, 31/10/07? We can use the format property of the DateField
component to display the date how we like:
<input type=”text” t:type=”datefield” t:value=”dateOfBirth”
t:format=”%d/%m/%y”/>
You can also construct a completely different date format. For example, %b %e, %Y
will produce the result Oct 31, 2007. For the complete list of formatting characters
check http://www.dynarch.com/demos/jscalendar/doc/html/reference.
html#node_sec_5.3.1, but the following are a few that might be most useful:
Formatting character | Its meaning |
%a | Abbreviated weekday name |
%A | Full weekday name |
%b | Abbreviated month name |
%B | Full month name |
%d | The day of the month (00…31) |
%e | The day of the month (0…31) |
%m | Month (01…12) |
%y | Year without the century (07) |
%Y | Year including the century (2007) |
Page 4 Of 6 –
Advanced Components from the book “Tapestry 5 – Building Web Applications”
We can use the DateField component everywhere we need to edit a property of
the java.util.Date type. Whenever the BeanEditForm components finds a
property of this type in the edited JavaBean, it selects DateField to edit that
property automatically.
Changing the Styles of Grid and
BeanEditForm
Everything works fine but looks less than perfect. For example, the labels of
BeanEditForm are cramped on the left side of the form so that lines like Birth Date
Verified have to split into two lines, and it misplaces the other labels as a result.
Also, the background color and the font used by default for Grid and BeanEditForm
might not fit the design of your application. Fortunately, the appearance of these
components is de fined by CSS styles, and so we can easily infl uence how they look
by changing the styles.
Tapestry provides a default stylesheet, named appropriately default.css, and this
is exactly where the styling of its components is de fined. Tapestry adds the default
stylesheet in such a way that it will always be the first for every page, and so any
stylesheet that we provide can override whatever is de fined in default.css. To
make the desired changes, we need to provide a stylesheet of our own and in it
declare the same styles as in default.css—with the same names, but with
different content.
As the first step, we might want to look into the default.css file. It can be found in
the Tapestry source code package that can be downloaded from http://tapestry.
apache.org/download.html. The name of the package will be similar to tapestrysrc
5.0.6 binary (zip). The default.css file can be found inside the package,
in the tapestry-core\src\main\resources\org\apache\tapestry subdirectory.
This file contains a signi ficant number of styles, but those related to BeanEditForm
have t-beaneditor in their name, and those related to Grid contain t-data-grid.
Let’s say we want to change the background of BeanEditForm to white, the
surrounding border to green and give more space to the labels. Admittedly, I am
not an expert in CSS, but it is not actually dif ficult to figure out what exactly should
be changed. These are the two style de finitions we shall want to tweak:
DIV.t-beaneditor
{
display: block;
background: #ffc;
border: 2px solid silver;
padding: 2px;
font-family: “Trebuchet MS”, Arial, sans-serif;
}
DIV.t-beaneditor LABEL
{
width: 10%;
display: block;
float: left;
text-align: right;
clear: left;
padding-right: 3px;
}
The first of them de fines the appearance of the form in general, the second—the
appearance of the labels used for the fields in the form. It is easy to guess that the
first de finition will allow us to change, most signi ficantly, the background, the
border and the font of the form, while the second allows us to change the space given
to the labels (currently only 10% of the width of the page).
But where do we put our own style de finitions? It will be convenient to have a
directory for all the assets of our web application, let’s name it appropriately, styles.
It should be created at the root of the web application, on the same level where page
templates are placed.
To create it in NetBeans, right click on the Web Pages folder inside the project
structure. Select New|File/Folder…, then Other in Categories and Folder in File
Types. Click on Next, give the new folder a name, and then click on Finish. Now
right click on the new styles folder and again select New|File Folder…. Choose
Other for Categories, Empty File for File Types, click on Next and name the new file
something like styles.css.
In Eclipse, the sequence of actions will be similar, but the new styles folder should be
added to the WebContent folder in the project structure.
Now we can put the aforementioned style de finitions into styles.css, and change
them as required. Let’s try something like this:
DIV.t-beaneditor
{
display: block;
background: white;
border: 2px solid green;
padding: 2px;
font-family: “Trebuchet MS”, Arial, sans-serif;
}
DIV.t-beaneditor LABEL
{
width: 150px;
display: block;
float: left;
text-align: right;
clear: left;
padding-right: 3px;
}
In fact, it should be enough to leave only the highlighted lines here as the other
details were already speci fied in the default style sheet.
We can also influence the positioning of the Save button:
input.t-beaneditor-submit
{
position: relative;
left: 150px;
}
However, to see the changes, we need to make the new stylesheet available to the
web page. Tapestry can inject an asset, be it a stylesheet or an image, into the page
class when we ask it to do that, so let’s add to the AddCelebrity page class to the
following lines of code:
@Inject
@Path(“context:styles/styles.css”)
private Asset styles;
public Asset getStyles()
{
return styles;
}
Finally, provide in the page template a link to the stylesheet:
<head>
<title>Celebrity Collector: Adding New Celebrity</title>
<link rel=”stylesheet” href=”${styles}” type=”text/css”/>
</head>
If you run the application now, you will notice a signi ficant difference in the
form’s appearance:
You can continue experimenting from here with styles on your own, using the
default.css file and the source code of the rendered page (where you can see
which styles are used for what) as your starting point.
But let me show you one more very useful component.
FCKEditor Component
Quite often, we might want to give the users of our application an opportunity
not only to enter a message, but also to format it similarly to how they would do
this in a familiar word processor. There are JavaScript-enabled rich text editors
available for this purpose, the most famous of them is perhaps FCKEditor (http://www.fckeditor.net/), but integrating such an editor into a web
application might require additional knowledge and effort.
Thankfully, there is a custom Tapestry 5 component developed by Ted Steen and
Olof Naessen that wraps FCKEditor. As a result, we can use this excellent rich text
editor in the same way like any other Tapestry component. The component can
be downloaded from http://code.google.com/p/tapestry5-fckeditor/ as a
JAR file (make sure you pick the version, 1.0.2 or later). The name of the file will be
similar to tapestry5-fckeditor-1.0.2.jar. Please put it into the WEB-INF/lib
subfolder of our web application. In Eclipse, you might want to press F5 to make
sure that the IDE has noticed the addition.
Let’s use the FCKEditor component to enter a celebrity’s biography at the
AddCelebrity page. All we need to do is to change the type of component used to
edit the biography property in the page template:
<t:parameter name=”biography”>
<t:label for=”biography”/>
<t:fckeditor.editor t:id=”biography”
t:value=”celebrity.biography”/>
</t:parameter>
The result of this change might look a bit overwhelming:
Page 5 Of 6 –
Advanced Components from the book “Tapestry 5 – Building Web Applications”
By default, the component is huge as it displays all the goodies made available by
the original FCKEditor. In many cases you will not need all that, and there is an
easy way to both change the number of displayed toolbars and change the size of
the component.
There are three toolbar sets available—Default, Simple and Medium, and we
can choose using the toolbarSet parameter. There are also width and height
parameters that allow us to specify the size of the component. Here is one possible
combination of settings:
<t:fckeditor.editor t:id=”biography”
t:value=”celebrity.biography”
t:toolbarSet=”Simple” t:width=”300″ t:height=”150″/>
Well, I have to admit that to get the component properly positioned in Internet
Explorer 7, I had to use some additional markup, to place the FCKEditor component
and its label inside a simple table, like this:
<t:parameter name=”biography”>
<table cellpadding=”0″ cellspacing=”0″>
<tr>
<td valign=”top”>
<t:label for=”biography”/></td>
<td>
<t:fckeditor.editor t:id=”biography”
t:value=”celebrity.biography”
t:toolbarSet=”Medium” t:width=”350″
t:height=”200″/></td>
</tr>
</table>
</t:parameter>
After the text of the biography is entered, formatted and submitted, what gets sent
to the server (and is stored in a property of the page class) is a piece of HTML with
all the markup that is needed to reproduce the formatting. We can later display the
biography on the details page, but right now we need to make sure that the new
information (and a new Celebrity object containing it) is stored properly in the
data source.
All we need to do for this is to add the following fragment of code to the
AddCelebrity page:
@ApplicationState
private IDataSource dataSource;
Object onSubmitFromCelebrity()
{
dataSource.addCelebrity(celebrity);
return ShowAll.class;
}
We are handling form submission here, so our event handler
handles the submit event and is named appropriately, according to
convention. Looks all right, doesn’t it? This will work for now, but
in the next chapter, we’ll find out that in such a situation we should
handle the “success” event. So the method should be better named
onSuccessFromCelebrity.
We have added an event handler and a reference to the data source ASO. When the
form with the new celebrity is submitted, we store the resulting Celebrity object
using the data source existing for this purpose and display the ShowAll page.
However, at the moment the table that displays our collection has two extra columns,
for biography, and birthDateVerified properties. We do not want to see them, so
let’s modify the ShowAll page template:
<t:grid t:source=”celebritySource” rowsPerPage=”5″
row=”celebrity”
remove=”id, biography, birthDateVerified”
reorder=”lastName,firstName,occupation,dateOfBirth”>
Finally, to get the biography displayed at the Details page, let’s make the necessary
preparations. All we need to do is to add the following piece of markup to the
page template:
<tr>
<td>Occupation:</td>
<td>${celebrity.occupation}</td>
</tr>
<tr>
<td valign=”top”>Biography:</td>
<td>
<t:outputraw t:value=”celebrity.biography”/>
</td>
</tr>
Now add a new celebrity and a brief biography for him or her as I did above for
John Lennon. Format the biography using different styles and colors to your heart’s
content and then click on the Save button. You will see the ShowAll page and the
newly added celebrity will be somewhere at the end of collection, perhaps on
page four.
Find the new celebrity and click on the last name to see the details. Depending on
how you formatted the biography, you should see something similar to this:
As you can see, all the formatting is displayed properly, and this is the reason why
we used a new component, OutputRaw in the last example. In fact, this component
is quite similar to an ordinary Output component, or even to a basic extension—it
simply outputs whatever is given to it as a value. The difference is that both regular
output and extension encode the content that they insert into the page while
OutputRaw just inserts into the resulting HTML its value, no matter what it contains.
For instance, if the value provided by the component’s binding is <b>bold text</b>,
then regular output will encode angle brackets and produce the following result: <b>bold text</b>. As a result, instead of formatted text, the page will
display the tags verbatim: <b> bold text</b>. The OutputRaw however will insert
into the page what was given to it, and as a result, we’ll see bold text.
Security Note: Please use the OutputRaw component with caution. If
you will use it to freely display any content entered by a random user,
someone might enter a hostile script and achieve sinister results that you
cannot even imagine.
Summary
- We have learned to use four powerful and useful components—Grid,
BeanEditForm, DateField and FCKEditor. They can save us a lot of work since with
minimal con figuration, they produce a rather sophisticated, functionally rich piece of
interface. We have also found out that: - We can change the way an object is displayed by Grid and BeanEditForm
components, in terms of which properties are displayed and how they
are ordered. - We can override the default rendering of a property by Grid or the default
editor selected for it by BeanEditForm, using the <t:parameter> element. - We can modify the titles of the columns in the Grid or the labels of the fields
in the BeanEditForm by providing appropriate messages in the application’s
message catalog.
We can change the appearance of the components by overriding the default CSS
styles in the stylesheet that we provide ourselves.
We already have several controls that accept user input in the application, but
nowhere have we checked what kind of input is submitted so far. This is acceptable
at the initial stage of development, when we are the only users of the application,
and we know for sure which kind of information should be entered in each field.
However, every real life application must validate user input, and if there are errors,
it should inform the user about them in a friendly and clear way. The next chapter
will discuss the powerful validation system of Tapestry 5.
Page 6 Of 6 –
Advanced Components from the book “Tapestry 5 – Building Web Applications”