Login

Apache Tapestry and Custom Components: DateInput

Tapestry has many benefits, but perhaps one of the most important is that it makes creation of custom components very easy. In this article, I’ll create a custom component to illustrate this point.

We are all different; we all have various ideas about how our application should look and work. Our customers have their own opinions and, well, sometimes prejudices too. As a result, at some time in your Tapestry development you will certainly come to the point when you’ll find the existing choice of components insufficient, and you might want to create a component of your own.

In a previous article we were playing with one of the core Tapestry components, DatePicker, and I tried to convince you that although this component is very good, it might not always be an ideal choice for date input. Today I am going to show you what I have done in a real development project when I needed an alternative way to accept a date input from the user.

The process of creating Tapestry components is very similar to the process of creating Tapestry pages. As a page is typically represented by three files, say, Home.html for the template, Home.page for the specification and Home.java for the class, in the same way a component is typically represented by three files as well: say, SomeComponent.html for the template, SomeComponent.jwc for the specification (jwc extension means “Java Web Component”) and SomeComponent.java for the class.

The only significant difference between components and pages is that components usually accept at least one parameter while pages don’t. Everything else is very, very similar.

Okay, that was enough propaganda; let’s get some practical experience now. First of all, we need to visualize what exactly we want to achieve. As our Swiss customer with a massive C programming background can’t bear a fancy DatePicker, we need to suggest something more traditional. Perhaps three drop-down lists for month, day and year input will do. This way, especially if we use not numbers but descriptive names for months, there will be no space for misinterpreting user input (as in that example – the 6th of December or the 12th of June?). Even if the user enters something like the 31st of February, by accepting the date in lenient mode we’ll automatically convert it into the second or third of March, as the case may be.

Here is a screen shot of what we want to create:

As we don’t want to place three PropertySelection components, configure them and then write some code to interpret them as one piece of input every time we need to accept a date from our user, what we need is a custom component. We want to be able to just drop it on a page and connect it to some property of the page class through the component’s binding. Let’s see how we can achieve this.

{mospagebreak title=Creating the DateInput Component}

Let the name for our custom component be DateInput – quite descriptive for its purpose. So the three files associated with this component will be DateInput.html, DateInput.jwc and DateInput.java. We can simply add these files to the existing CelebrityCollector project, in the same way we added pages to it before. We might decide to use this new DateInput as a replacement for the DatePicker component.

The first step is to create an HTML template, DateInput.html file. It is actually a fragment of HTML we want to be included into an HTML page when the component renders itself, but it will often make sense to create the component template as a complete standalone HTML page so that we can easily preview it in any browser:

<html>

<head>

<title>DateInput Template</title>

</head>

<body>

<select>

<option>January</option>

<option>February</option>

</select>

<select>

<option>1</option>

<option>2</option>

</select>,

<select>

<option>2005</option>

<option>2006</option>

</select>

</body>

</html>

Now let’s convert this mock up into a Tapestry template. All three <select> elements should obviously be marked as Tapestry components, but there is also one other thing to do. We made the template a complete HTML page, but we don’t want all those surrounding <html>, <head> etc. tags to be inserted somewhere in the middle of another page.

Fortunately, Tapestry has a special tool that allows us to do exactly what we want, and very easily. Have a look at this template:

<html>

<head>

<title>DateInput Template</title>

</head>

<body jwcid=”$content$”>

<select jwcid="month">

<option>January</option>

<option>February</option>

</select>

<select jwcid="day">

<option>1</option>

<option>2</option>

</select>,

<select jwcid="year">

<option>2005</option>

<option>2006</option>

</select>

</body>

</html>

Note the jwcid=”$content$” attribute, used here to mark the <body> element. It tells Tapestry that anything surrounded by this element should be taken seriously (i.e. rendered at runtime) while everything else, the <body> itself and everything that surrounds it, should be discarded.

The next step is to create a component specification. It looks very similar to a page specification:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE component-specification PUBLIC

"-//Apache Software Foundation//Tapestry Specification 4.0//EN"

"http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">

<component-specification

class="com.devshed.tapestry.celebrities.DateInput">

<description>Component for choosing a date</description>

<component id="day" type="PropertySelection">

<binding name="model" value="daysModel"/>

<binding name="value" value="day"/>

</component>

<component id="month" type="PropertySelection">

<binding name="model" value="monthsModel"/>

<binding name="value" value="month"/>

</component>

<component id="year" type="PropertySelection">

<binding name="model" value="yearsModel"/>

<binding name="value" value="year"/>

</component>

</component-specification>

In fact, the only difference is the name of the root element: it is now <component-specification> instead of <page-specification>. Otherwise, the specification is exactly the same as it would be for a Tapestry page with three PropertySelection components in it.

{mospagebreak title=Creating the Models}

As you already know pretty well, we have to give PropertySelection components their models, explaining to them what exactly to display and what to return to the page class when the user makes a selection. The model for days is very simple and can be implemented in a few different ways. Here is one of the options:

public class DayModel implements IPropertySelectionModel {

public String getLabel(int index) {

return "" + (index + 1);

}

public Object getOption(int index) {

return index + 1;

}

public int getOptionCount() {

return 31;

}

public String getValue(int index) {

return "" + index;

}

public Object translateValue(String value) {

return Integer.parseInt(value) + 1;

}

}

We are simply displaying numbers from 1 to 31 as labels and use the indexes of the options for value attributes. I don’t feel like any explanations are needed here, but if you do have a question, please ask it at the discussions page. In any case, reviewing the article devoted to IPropertySelectionModel might be useful.

The model for selecting a year is also quite simple:

public class YearModel implements IPropertySelectionModel {

private final static int START_YEAR = 1900;

private final static int END_YEAR =

new GregorianCalendar().get(Calendar.YEAR);

public String getLabel(int index) {

return Integer.toString(index + START_YEAR);

}

public Object getOption(int index) {

return index + START_YEAR;

}

public int getOptionCount() {

return END_YEAR – START_YEAR + 1;

}

public String getValue(int index) {

return "" + index;

}

public Object translateValue(String value) {

return Integer.parseInt(value) + START_YEAR;

}

}

Everything should be more or less clear to you here, or if not, welcome to the discussions page. The model for months is slightly more interesting because it returns months’ names using the existing locality settings. For example, they will be automatically displayed in Spanish if your preferred language is Spanish, etc. This means that DateInput can be very easily localized, and we shall discuss this in more detail in a coming article on internationalization. For now, just have a look at the code and, of course, add this class to your application:

Finally, we need to create a component class. It will bring together all the constituents and make them work together toward the goal that we’ve defined:

public abstract class DateInput extends BaseComponent implements

PageBeginRenderListener {

@Parameter(required = true)

public abstract Date getDate();

public abstract void setDate(Date d);

private Calendar c = Calendar.getInstance();

public void pageBeginRender(PageEvent event) {

c.setTime(getDate() == null ? new Date() : getDate());

}

public IPropertySelectionModel getDaysModel() {

return new DayModel();

}

public IPropertySelectionModel getMonthsModel() {

return new LocalisedMonthsModel(getPage().getLocale());

}

public IPropertySelectionModel getYearsModel() {

return new YearModel();

}

public int getDay() {

return c.get(Calendar.DATE);

}

public void setDay(int day) {

c.set(Calendar.DATE, day);

}

public int getMonth() {

return c.get(Calendar.MONTH);

}

public void setMonth(int month) {

c.set(Calendar.MONTH, month);

}

public int getYear() {

return c.get(Calendar.YEAR);

}

public void setYear(int year) {

c.set(Calendar.YEAR, year);

setDate(c.getTime());

}

}

Most of this code is very simple, but there are a few points that are worth commenting upon. There are three methods returning models to PropertySelection components, and one of them is slightly unusual:

public IPropertySelectionModel getMonthsModel() {

return new LocalisedMonthsModel(getPage().getLocale());

}

Here you can see how our component gets a reference to the page, onto which it will be placed (the getPage() method is available to every component), and then asks the page for the current Locale. The page’s answer is then passed to the LocalisedMonthsModel constructor, so that the names for the months were shown in an appropriate language.

Perhaps the most interesting fragment of this code is how we define the parameter required by our custom component:

@Parameter(required = true)

public abstract Date getDate();

public abstract void setDate(Date d);

You should already be used to abstract getters and setters – our way of asking Tapestry to implement whatever is needed automatically. Here we also clarify that this will be a parameter named date, and this parameter is required. This means that if someone will want to use DateInput, they will need to provide a binding named date, and if they don’t, Tapestry will throw an exception.

Using the abstract getter, we can obtain in our code the value provided by the page class through the required binding, and when the user submits the results of his or her input, we’ll use the abstract setter to report to the page on which the date was selected.

{mospagebreak title=Implementing PageBeginRenderListener}

To manipulate dates, we use an instance of java.util.Calendar class and simply get or set appropriate values using the appropriate keys (like Calendar.YEAR). In the very beginning, we need to set this Calendar to some initial value. In the simplest case, we just set it to the value received from the parameter (through getDate() method). Quite often, however, the initial value can be null. What should we do in this case? One reasonable option is to display the current date, hence the code:

c.setTime(getDate() == null ? new Date() : getDate());

But when should we run this line of code? Perhaps just before the DateInput component will have to render itself, as it will need to know which values to display. We can imagine that our component tells the page: “Hey, Page, could you please let me know when you’re going to render me? I am going to do some initialization just before that.” Translated into Tapestry API, this will make our component a PageBeginRenderListener (you see, the component class implements this interface).

The PageBeginRenderListener interface has only one method, pageBeginRender(), and this method will be invoked by the page just before it begins rendering itself and any components contained by itself.

Now that we’ve sorted out initialization, the PropertySelection components displaying the day, month and year can obtain their values from the Calendar and report whatever was selected by the user back to the Calendar.

When the page is submitted to the server, the setters will work exactly in the order in which the components are placed: setMonth(), then setDay() and finally setYear(). Since setYear() runs last, we made it responsible for setting the value of the date parameter to whatever was put into the Calendar by this time:

public void setYear(int year) {

c.set(Calendar.YEAR, year);

setDate(c.getTime());

}

This completes the explanation of how the DateInput component works. The explanation was somewhat verbose, but only because I wanted to explain every detail. All we need to do now is put this component on some page and test it.

One possible solution is to replace the existing DatePicker component. It is used on the AddCelebrity page and configured like this:

<component id="dateOfBirth" type="DatePicker">

<binding name="value" value="dateOfBirth"/>

</component>

To make the change, we just edit the type of component and the name for its binding:

<component id="dateOfBirth" type="DateInput">

<binding name="date" value="dateOfBirth"/>

</component>

Now you can run the application, add some new celebrity, and the date-related functionality should work exactly as it worked before, with the only difference that we don’t need any JavaScript harness now and there is no way to misinterpret user input.

To summarize, I think you will agree that creating a fully functional custom component wasn’t more difficult than creating a simple Tapestry page. We did have to write a number of methods, but most of them contained just one line of code (and the remaining few contained two lines of code).

We could further enhance the DateInput component – for example, add a parameter for disabling it, or enable tabulation through its drop-down lists. This however has to be left until a later article, or you might want to try and do this yourself.

What comes next

In the next article I am planning to show you how using the NBTapestry module for NetBeans can make our life somewhat easier. I also plan to demonstrate different ways of submitting forms in Tapestry, something I have been putting off since the very beginning of the series.