Table of Contents
Now it's time to build a real, interactive application. We'll still use just a single page, but it will demonstrate many of the more interesting features of Tapestry, including maintenance of server side page state.
Our Adder application allows the user to sum up a list of numbers.
The user enters a number into the value field and clicks "Add to list". The number is added to the list of items and factored into the total.
A Form component containing a TextField component will be used to collect information from the user. A Foreach component will be used to run though the list of items, and Insert components will be used to present each item in the list, as well as the total.
If the user enter in a non-number, then an error message is displayed.
As with the previous examples, the servlet and application objects are simple variations on the previous two sets (they are ommited here).
The application specification is, likewise, a variation on the prior example.
The code for this section is in the tutorial.adder package.
We'll start with the HTML template for the home page:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Adder Tutorial</title> </head> <body> <jwc id="ifError"> <table border=1> <tr> <td bgcolor=red> <span style="font: bolder 14pt; color:white"> <jwc id="insertError"/> </span> </td> </tr> </table> <p> </jwc> <jwc id="form"> <table> <tr> <td align=right>Value:</td> <td><jwc id="inputNewValue"/></td> </tr> <tr> <td> </td> <td><input type=submit value="Add to list"></td> </tr> </table> </jwc> <table> <tr> <th>Items</th> </tr> <jwc id="e"> <tr align=right> <td> <jwc id="insertCurrentValue"/> </td> </tr> </jwc> <tr align=right> <td> <hr> <br><jwc id="insertTotal"/> </td> </tr> </table> </body> </html> |
Again, Tapestry takes care of most of the details. The form component will turn into an HTML <form> element, and the correct URL is automatically generated. The inputNewValue component will become an <input type=text>, with the necessary smarts to collect the value submitted by the user and provide it to the page.
The e component is type Foreach, used for running through a list of elements (supplied as a List, Iterator or an array of Java objects). We've already see the Insert component.
Next we have the specification:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE specification PUBLIC "-//Howard Ship//Tapestry Specification 1.1//EN" "http://tapestry.sf.net/dtd/Tapestry_1_1.dtd"> <specification class="tutorial.adder.Home"> <component id="ifError" type="Conditional"> <binding name="condition" property-path="error"/> </component> <component id="insertError" type="Insert"> <binding name="value" property-path="error"/> </component> <component id="form" type="Form"> <binding name="listener" property-path="formListener"/> </component> <component id="inputNewValue" type="TextField"> <binding name="text" property-path="newValue"/> </component> <component id="e" type="Foreach"> <binding name="source" property-path="items"/> </component> <component id="insertCurrentValue" type="Insert"> <binding name="value" property-path="components.e.value"/> </component> <component id="insertTotal" type="Insert"> <binding name="value" property-path="total"/> </component> </specification> |
We only want to display the error message if there is one, so the ifText is conditional on there being a non-null error message (the Conditional component treats null as false).
For the form component, all we have to do is supply a listener, an object that is informed when the form is submitted.
For the inputNewValue component, we provide a text parameter that provides the default value for the <input> element. The same property is updated with the value submitted with the form.
The property must be of type String, so we need to do a little translation (in our Java class), since internally we want to store the value as a double.
For the e component, we supply a binding for the source parameter. For each item in the source list, it will update its own value property, which is later accessed by the insertCurrentValue component. The property path components.e.value accomplishes this: the page has a components property, which is a Map of the components on the page. e is the id of a component, and a key in the Map. It has a property named value, which is the current item from the source list.
A Foreach component also has a parameter named value. By creating a binding for this parameter, the Foreach can update a property of the page, or some other component. This is more commonly used when the items in the list are business objects and the application needs to invoke business methods on them.
Finally, the Java code for the home page puts everything together:
package tutorial.adder; import com.primix.tapestry.*; import com.primix.tapestry.components.*; import java.util.*; public class Home extends BasePage { private List items; private String newValue; private String error; public List getItems() { return items; } public void setItems(List value) { items = value; fireObservedChange("items", value); } public void setNewValue(String value) { newValue = value; } public String getNewValue() { return newValue; } public void detach() { items = null; newValue = null; error = null; super.detach(); } public void beginResponse(IResponseWriter writer, IRequestCycle cycle) throws RequestCycleException { super.beginResponse(writer, cycle); if (items == null) setItems(new ArrayList()); } public void addItem(double value) { if (items == null) { items = new ArrayList(); fireObservedChange("items", items); } items.add(new Double(value)); fireObservedChange(); } public double getTotal() { Iterator i; Double item; double result = 0.0; if (items != null) { i = items.iterator(); while (i.hasNext()) { item = (Double)i.next(); result += item.doubleValue(); } } return result; } public IActionListener getFormListener() { return new IActionListener() { public void actionTriggered(IComponent component, IRequestCycle cycle) { try { double item = Double.parseDouble(newValue); addItem(item); newValue = null; } catch (NumberFormatException e) { error = "Please enter a valid number."; } } }; } public String getError() { return error; } } |
That may seem like a lot of code for what we're doing, but in reality, very much is going that we don't have to write:
Tapestry components, using JavaBeans properties, take care of moving data to and from the HTML form. Our application merely has to supply the logic to properly respond when the form is submitted. In this case, converting the text into a double that can be added to the list.
Because we let Tapestry set the names of our form elements, there's no possibility of mismatched names between the Java code (setting defaults and interpreting the posted request) and the HTML template.
Enter a few values into the text field to see how the application works, adding them together into an ever larger list.
To understand the relationship between the home page specification, the home page class and the components used by the home page, it is necessary to understand the JavaBeans properties provided by the home page class.
We implement several JavaBeans properties on this page:
Table 5.1. JavaBeans Properties
Property name | Type | R / W | Description |
---|---|---|---|
newItem | String | R / W | Stores the string entered into the form. |
items | List (of Double) | R / W | Items in the list. Persists between request cycles. |
formListener | IActionListener | Read Only | Informed when form is submitted. |
total | double | Read Only | Total of items; computed on the fly. |
This example demonstrates how to provide interactivity to an application. For Tapestry, interactivity is defined as a request cycle initiated by a user clicking on a hyperlink or submitting a form.
In our case, we want to know when the form containing the text field is submitted so that we can provide application specific behavior: adding the value enterred by the user to the list of items.
This is accomplished using a listener, an object that implements the Java interface IActionListener. This interface defines a single method, actionTriggered(). When the form is submitted, all the components wrapped by the form (in this case, the TextField) are given a chance to retrieve their values from the request and update properties of the application (the TextField sets the currentItem property). The form then gets its listener and invokes the actionTriggered() method.
In the specification, the listener parameter was bound to the formListener property of the page. The code in the getFormListener() method creates an anonymous inner class and returns it.
Inner classes have access to the private fields and methods of the class. In this case, the inner class invokes the addItem() method to add the currentItem (with a value provided by the TextField component) to the items List.
A listener is free to do anything it wants. It can change the state of the application, or can retrieve other pages (by name) from the request cycle object, and can change properties of those pages. It can even chose a different page to render, by invoking setPage() on the request cycle.