Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Fix next link

...

Scrollbar

In the previous chapters, we saw how Tapestry can handle simple links, even links that pass information in the URL. In this chapter, we'll see how Tapestry can do the same, and quite a bit more, for HTML forms.

...

We'll start with the entity data, a simple object to store the information we'll need. These classes go in an entities sub-package. Unlike the use of the pages sub-package (for page component classes), this is not enforced by Tapestry; it's just a convention (but as we'll see shortly, a handy one).

Tapestry treats public fields as if they were JavaBeans properties; since the Address object is just "dumb data", there's no need to get carried away writing getters and setters. Instead, we'll define an entity that is all public fields:

Code Block
languagejava
titlesrc/main/java/

...

com/

...

example/

...

tutorial/entities/Address.java

...

Code Block

package orgcom.apacheexample.tapestry5tutorial1.tutorial.entities;

import orgcom.apacheexample.tapestry5tutorial1.tutorial.data.Honorific;

public class Address
{
  private  public Honorific honorific;

    privatepublic String firstName;

    privatepublic String lastName;

    privatepublic String street1;

    privatepublic String street2;

    privatepublic String city;

    privatepublic String state;

    privatepublic String zip;

    privatepublic String email;

  private  public String phone;
}
  public String getCity()
  

We also need to define the enum type, Honorific:

Code Block
languagejava
titlesrc/main/java/com/example/tutorial/data/Honorific.java
package com.example.tutorial1.data;

public enum Honorific
{
    return city;
  }

  public String getEmail()
  {
    return email;
  }

  public String getFirstName()
  {
    return firstName;
  }

  public Honorific getHonorific()
  {
    return honorific;
  }

  public String getLastName()
  {
    return lastName;
  }

  public String getPhone()
  {
    return phone;
  }

  public String getState()
  {
    return state;
  }

  public String getStreet1()
  {
    return street1;
  }

  public String getStreet2()
  {
    return street2;
  }

  public String getZip()
  {
    return zip;
  }

  public void setCity(String city)
  {
    this.city = city;
  }

  public void setEmail(String email)
  {
    this.email = email;
  }

  public void setFirstName(String firstName)
  {
    this.firstName = firstName;
  }

  public void setHonorific(Honorific honorific)
  {
    this.honorific = honorific;
  }

  public void setLastName(String lastName)
  {
    this.lastName = lastName;
  }

  public void setPhone(String phone)
  {
    this.phone = phone;
  }

  public void setState(String state)
  {
    this.state = state;
  }

  public void setStreet1(String street1)
  {
    this.street1 = street1;
  }

  public void setStreet2(String street2)
  {
    this.street2 = street2;
  }

  public void setZip(String zip)
  {
    this.zip = zip;
  }
}

It's just a collection of getter and setter methods. We also need to define the enum type, Honorific:

src/main/java/org/apache/tapestry5/tutorial/data/Honorific.java:

Code Block

package org.apache.tapestry5.tutorial.data;

public enum Honorific
{
  MR, MRS, MISS, DR
}

Address Pages

We're probably going to create a few pages related to addresses: pages for creating them, for editing them, for searching and listing them. We'll create a sub-folder, address, to hold them. Let's get started on the first of these pages, "address/Create" (that's the real name, including the slash — we'll see in a minute how that maps to classes and templates).

First, we'll update the Index.tml template, to create a link for creating a new page:

src/main/webapp/Index.tml:

...


    <h1>Address Book</h1>

    <ul>
      <li><t:pagelink page="address/create">Create new address</t:pagelink></li>
    </ul>

Now we need the page, let's start with an empty shell, just to test our navigation.

src/main/webapp/address/CreateAddress.tml:

...


<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd">
  <head>
    <title>Create New Address</title>
  </head>
  <body>

    <h1>Create New Address</h1>

    <em>coming soon ...</em>

  </body>
</html>
MR, MRS, MISS, DR
}

Address Pages

We're probably going to create a few pages related to addresses: pages for creating them, for editing them, for searching and listing them. We'll create a sub-folder, address, to hold them. Let's get started on the first of these pages, "address/Create" (that's the real name, including the slash — we'll see in a minute how that maps to classes and templates).

First, we'll update the Index.tml template, to create a link to the new page:

Code Block
languagexml
titlesrc/main/resources/com/example/tutorial/pages/Index.tml (partial)
    <h1>Address Book</h1>

    <ul>
        <li><t:pagelink page="address/create">Create new address</t:pagelink></li>
    </ul>

Now we need the address/Create page; lets start with an empty shell, just to test our navigation.

Code Block
languagexml
titlesrc/main/resources/com/example/tutorial/pages/address/CreateAddress.tml
<html t:type="layout" title="Create New Address"
    xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">

    <em>coming soon ...</em>

</html>

(Note: for Tapestry 5.4, make that tapestry_5_4.xsd instead.)

Next, the corresponding class:

Code Block
languagejava
titlesrc/main/java/com/example/tutorial/pages/address/CreateAddress.java
package com.example.tutorial1.pages.address;

public class CreateAddress
{

}

So ... why is the class named "CreateAddress" and not simply "Create"? Actually, we could have named it "Create", and the application would still work, but the longer class name is equally valid. Tapestry noticed the redundancy in the class name (com.example.tutorial1.pages.address.CreateAddress) and just stripped out the redundant suffix.

Tapestry actually creates a bunch of aliases for you pages; any of these aliases are valid and can appear in URLs or in the page parameter of PageLink.  You can see the list in the console:

Code Block
[INFO] TapestryModule.ComponentClassResolver Available pages (12):
              (blank): com.example.tutorial1.pages.Index
   ComponentLibraries: org.apache.tapestry5.corelib.pages.ComponentLibraries
             Error404: com.example.tutorial1.pages.Error404
      ExceptionReport: org.apache.tapestry5.corelib.pages.ExceptionReport
             GameOver: com.example.tutorial1.pages.GameOver
                Guess: com.example.tutorial1.pages.Guess
                Index: com.example.tutorial1.pages.Index
          PageCatalog: org.apache.tapestry5.corelib.pages.PageCatalog
PropertyDisplayBlocks: org.apache.tapestry5.corelib.pages.PropertyDisplayBlocks
   PropertyEditBlocks: org.apache.tapestry5.corelib.pages.PropertyEditBlocks
        ServiceStatus: org.apache.tapestry5.corelib.pages.ServiceStatus
          T5Dashboard: org.apache.tapestry5.corelib.pages.T5Dashboard
       address/Create: com.example.tutorial1.pages.address.CreateAddress
address/CreateAddress: com.example.tutorial1.pages.address.CreateAddress

Tapestry users the shortest alias when constructing URLs.

Eventually, your application will probably have more entities: perhaps you'll have a "user/Create" page and a "payment/Create" page and an "account/Create" page. You could have a bunch of different classes all named Create spread across a number of different packages. That's legal Java, but it isn't ideal. You may find yourself accidentally editing the Java code for creating an Account when you really want to be editing the code for creating a Payment.

Tapestry is encouraging you to use a more descriptive name: CreateAddress, not just Create, but it isn't making you pay the cost (in terms of longer, uglier URLs). The URL to access the page will still

And the corresponding class:

src/main/java/org/apache/tapestry5/tutorial1/pages/address/CreateAddress.java:

Code Block

package org.apache.tapestry5.tutorial.pages.address;

public class CreateAddress
{

}

So ... why is the class named "CreateAddress" and not simply "Create"? Actually, we could have named it "Create", and the application would still work, but the longer class name is equally valid. Tapestry noticed the redundancy in the class name: org.apache.tapestry5.tutorial.pages.address.CreateAddress and just stripped it out.

Eventually, your application will probably have more entities: perhaps you'll have a "user/Create" page and a "payment/Create" page and an "account/Create" page. Now, you could have a bunch of different classes named Create spread across a number of different packages. That's legal Java, but it isn't ideal. You may find yourself accidentally editing the Java code for creating an Account when your really want to be editing the code for creating a Payment.

Tapestry is encouraging you to use a more descriptive name: CreateAddress not just Create, but it isn't making you pay the cost (in terms of longer, uglier URLs). The URL to access the page will still be http://localhost:8080/tutorial1/address/create.

Another note: Index pages work in folders as well. A class named org.apache.tapestry5.tutorial.pages.address.AddressIndex would be given the name "address/Index". However, Tapestry has special rules for pages named "Index" and the render URL would be http://localhost:8080/tutorial1/address/ . In other words, you can place Index pages in any folder and Tapestry will build a short URL for that page ... and you don't have to keep naming the classes Index (it's confusing to have many classes with the same name, even across multiple packages); instead, you can name each index page after the package that contains it. Tapestry users a smart convention to keep it all straight and generate short, to the point URLs.

Using the BeanEditForm component

Time to start putting together the logic for this form. In fact, let's use a magic trick ... the BeanEditForm component. This component can analyze a class and create an editor UI for it all in one go. Let's give it a try.

Add the following to the CreateAddress template (replacing the "coming soon ..." message):

...


  <t:beaneditform object="address"/>{noformat}

And match that up with a property in the CreateAddress class:

create.

And remember, regardless of the name that Tapestry assigns to your page, the template file is named like the Java class itself: CreateAddress.tml.

Info

Index pages work in folders as well. A class named com.example.tutorial1.pages.address.AddressIndex would be given the name "address/Index". However, Tapestry has special rules for pages named "Index" and the rendered URL would be http://localhost:8080/tutorial1/address/. In other words, you can place Index pages in any folder and Tapestry will build a short URL for that page ... and you don't have to keep naming the classes Index (it's confusing to have many classes with the same name, even across multiple packages); instead, you can name each index page after the package that contains it. Tapestry users a smart convention to keep it all straight and generate short, to the point URLs.

Using the BeanEditForm Component

Time to start putting together the logic for this form. Tapestry has a specific component for client-side Forms: the Form component, as well as components for form controls, such as Checkbox and TextField. We'll cover those in a bit more detail later .. instead, we're again going to let Tapestry do the heavy lifting for us, via the BeanEditForm component.

Add the following to the CreateAddress template (replacing the "coming soon ..." message):

Code Block
languagexml
titleCreateAddress.tml (partial)
    <t:beaneditform object="address"/>

And match that up with a property in the CreateAddress class:

Code Block
languagejava
titleCreateAddress.java (partial)
    @Property
    private Address address;

When you refresh the page, you may see a warning like the following at the top of the page:

Image Added

If you see that, it means you need to invent an HMAC passphrase for your app. Just edit your AppModule.java class (in your services package), adding a couple of lines to the contributeApplicationDefaults method like the following:

Code Block
        // Set the HMAC pass phrase to secure object data serialized to client
        configuration.add(SymbolConstants.HMAC_PASSPHRASE, "");

but, instead of an empty string, insert a long, random string of characters (like a very long and complex password, at least 30 characters) that you keep private.

After you do that, stop the app and restart it, and click on the Create new address link again, and you'll see something like this:

Image Added

Tapestry has done quite a bit of work here. It has created a form that includes a field for each property. Further, it has seen that the honorific property is an enumerated type, and presented that as a drop-down list.

In addition, Tapestry has converted the property names ("city", "email", "firstName") to user presentable labels ("City", "Email", "First Name"). In fact, these are <label> elements, so clicking a label with the mouse will move the input cursor into the corresponding field.

This is an awesome start; it's a presentable interface, quite nice in fact for a few minute's work. But it's far from perfect; let's get started with some customizations.

Changing Field Order

The BeanEditForm must guess at the right order to present the fields; for public fields, they end up in alphabetical order. For standard JavaBeans properties, the BeanEditForm default is in the order in which the getter methods are defined in the class (it uses line number information, if available).

A better order for these fields is the order in which they are defined in the Address class:

  • honorific
  • firstName
  • lastName
  • street1
  • street2
  • city
  • state
  • zip
  • email
  • phone

We can accomplish this by using the reorder parameter of the BeanEditForm component, which is a comma separated list of property (or public field) names:

Code Block
languagexml
titleCreateAddress.tml (partial)
    <t:beaneditform object="address"
        reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone" />

Image Added

Customizing labels

Tapestry makes it pretty easy to customize the labels used on the fields. It's just a matter of creating a message catalog for the page.

In Tapestry, every page and component may have its own message catalog. This is a standard Java properties file, and it is named the same as the page or component class, with a ".properties" extension. A message catalog consists of a series of lines, each line is a message key and a message value separated with an equals sign.

All it takes is to create a message entry with a particular name: the name of the property suffixed with "-label". As elsewhere, Tapestry is forgiving of case.

No Format
titlesrc/main/resources/com/example/tutorial/pages/address/CreateAddress.properties

@Property
private Address address

Code Block


When you refresh the page, you'll see the following:

!address-v1.png|border=1,width=760,height=439!

Initial version of the create address form

_There have been minor changes to the default CSS since this screenshot was taken; for example, the labels are a bit wider now. In addition, the Honorific field (being optional) would include a blank option, rather than the first real selection, "Mr"._

Tapestry's done quite a bit of work here. It has created a form that includes a field for each property. Further, its seen that the honorific property is an enumerated type, and presented that as a drop-down list.

In addition, Tapestry has converted the property names ("city", "email", "firstName") to user presentable labels ("City", "Email", "First Name"). In fact, these are <label> elements, so clicking a label will move the cursor into the corresponding field.

This is an awesome start; it's a presentable interface, quite nice in fact for a few minute's work. But it's far from perfect; let's get started with some customizations.

h3. Changing field order

It looks like the fields are being displayed in alphabetical order, ("city" first, "zip" last). That's not quite the reality, however: If you check the listing for the Address class, you'll see that the getter and setter methods are in alphabetical order (care of Eclipse, which generated all those methods from the fields).

The BeanEditForm works in the order in which the _getter methods_ are defined in the class. Let's reorder them into a more reasonable order:

* honorific
* firstName
* lastName
* street1
* street2
* city
* state
* zip
* email
* phone
(This is also the order of in which the fields are defined.)

Because Address is not a component class, it is necessary to restart Jetty to see the effects of these changes.

Once Jetty is restarted, hit the browser's refresh button to see the fields in the correct order:


!address-v2.png|border=1,width=760,height=439!
Create address form with fields in proper order

h3. Customizing labels

Tapestry makes it pretty easy to customize the labels used on the fields. It's just a matter of creating a _message catalog_ for the page.

In Tapestry, every page and component may have its own message catalog. This is a standard Java properties file, and it is named the same as the page or component class, with a ".properties" extension. A message catalog consists of a series of lines, each line is a message key and a message value separated with an equals sign.

All it takes is to create a message entry with a particular name: the name of the property suffixed with "-label". As elsewhere, Tapestry is forgiving of case.

*src/main/resources/org/apache/tapestry5/tutorial/pages/address/CreateAddress.properties:*

{noformat}
street1-label=Street 1
street2-label=Street 2
email-label=E-Mail
zip-label=Zip Code
phone-label=Phone Number{noformat}

Since

...

this

...

is

...

a

...

new

...

file

...

(and

...

not

...

a

...

change

...

to

...

an

...

existing

...

file),

...

you

...

may

...

have

...

to

...

restart

...

Jetty

...

to

...

force

...

Tapestry

...

to

...

pick

...

up

...

the change.

Image Added

We can also customize the options in the drop down list. All we have to do is add some more entries to the message catalog matching the enum names to the desired labels. Update CreateAddress.properties and add:

No Format
 change.


!address-v3.png|border=1,width=760,height=446!
Create Address form with field labels corrected

We can also customize the options in the drop down list. All we have to do is add some more entries to the message catalog matching the enum names to the desired labels. Update CreateAddress.properties and add:

{noformat}
MR=Mr.
MRS=Mrs.
DR=Dr.{noformat}

Notice

...

that

...

we

...

don't

...

have

...

to

...

include

...

an

...

option

...

for

...

MISS,

...

because

...

that

...

is

...

converted

...

to

...

"Miss"

...

anyway.

...

You

...

might

...

just

...

want

...

to

...

include

...

it

...

for

...

sake

...

of

...

consistency

...

...

...

the

...

point

...

is,

...

each

...

option

...

label

...

is

...

searched

...

for

...

separately.

...

Lastly,

...

the

...

default

...

label

...

on

...

the

...

submit

...

button

...

is

...

"Create/Update"

...

(BeanEditForm

...

doesn't

...

know

...

how

...

it

...

is

...

being

...

used).

...

Let's

...

change

...

that

...

to

...

"Create

...

Address".

...

That

...

button

...

is

...

a

...

component

...

within

...

the

...

BeanEditForm

...

component.

...

It's

...

not

...

a

...

property,

...

so

...

we

...

can't

...

just

...

put

...

a

...

message

...

into

...

the

...

message

...

catalog,

...

the

...

way

...

we

...

can

...

with

...

the

...

fields.

...

Fortunately,

...

the

...

BeanEditForm

...

component

...

includes

...

a

...

parameter

...

expressly

...

for

...

re-labeling

...

the

...

button.

...

Simply

...

change

...

the

...

CreateAddress

...

component

...

template:

Code Block
languagexml
  

{code:XML}
  <t:beaneditform submitlabel="Create Address" object="address"
        reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone"/>

The default for the submitlabel parameter is "Create/Update", but here we're overriding that default to a specific value.

The final result shows the reformatting and relabeling:relabelling:

Image AddedImage Removed
Create Address form with proper labels

Before continuing on to validation, a side note about message catalogs. Message catalogs are not just for re-labeling fields and options; we'll see in later chapters how message catalogs are used in the context of localization and internationalization.

...

In Tapestry, when binding a parameter, the value you provide may include a prefix. The prefix guides Tapestry in how to interpret the rest of the the parameter value ... is it the name of a property? The id of a component? A message key? Most fields parameters have a default prefix, usually "prop:", that is used when you fail to provide one (this helps to make the templates as terse as possible).

Here we want to reference a message from the catalog, so we use the "message:" prefix:

Code Block
XML
languageXMLxml

    <t:beaneditform object="address" submitlabel="message:submit-label" object="address"/>
        reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone" />

And then we define the submit-label key in the message catalog:

No Format

submit-label=Create Address

At then end of the dayIn the end, the exact same HTML is sent to the client, regardless of whether you include the label text directly in the template, or indirectly in the message catalog. In the long term, the latter approach will work better if you later chose to internationalize your application.

Adding Validation

Before we worry about storing the Address object, we should make sure that the user provides reasonable values. For example, several of the fields should be required, and phone numbers and email address have specific formats.

The BeanEditForm checks for a Tapestry-specific annotation, @org.apache.tapestry5.beaneditor.annotation, @Validate, on the field, the getter method, or the setter method of each property.

Update the getter methods for Edit the Address entity, and update the lastName, firstName, street1, city, state and zip fields, adding a @Validate annotation to each:

Code Block
languagejava
  
  @Validate("required")
    public String getFirstName()
  {
    return firstName;
  }

What is that string, "required"? That's how you specify the desired validation. It is a series of names that identify what type of validation is desired. A number of validators are built in, such as "required", "minLength" and "maxLength". As elsewhere, Tapestry is case insensitive.

You can apply multiple validations, by separating the validator names with commas. Some validators can be configured (with an equals sign). Thus you might say "required,minLength=5" for a field that must be specified, and must be at least five characters long.

Restart the application, and refresh your browser, then hit the submit button.

Image Removed

Form with client side validations visible

This is a shot just after hitting the submit button; all the fields have been validated and pop-up error bubbles are displayed. This looks a bit cluttered, but all the bubbles, except for the one for the focus field (the field the user is actively typing into), will fade out after a moment. As you tab from field to field, Tapestry will validate your input and briefly display the error bubble. And all of this is taking place on the client side, without any communication with the application.

must be specified, and must be at least five characters long.

Warning

You can easily get confused when you make a change to an entity class, such as adding the @Validate annotation, and not see the result in the browser. Only component classes, and (most) classes in the Tapestry services layer, are live-reloaded. Data and entity objects are not reloaded, so this is one area where you need to stop and restart Jetty to see the change.

Restart the application, and refresh your browser, then hit the Create Address button.

Image Added

This is a shot just after hitting the Create Address button; all the fields have been validated and errors displayed. Each field in error has been highlighted (it's a bit subtle) and marked with a red "X"in red and had an error message added. Further, the label for each of the fields has also been highlighted in red, to even more clearly identify what's in error. The cursor has also been moved to the first field that's in error. And all of this is taking place on the client side, without any communication with the application.

Once all the errors are corrected, and the form does submit, all validations are performed on the server side as well (just in case the client has JavaScript disabled).

So ... how about some more interesting validation than just "required or not". Tapestry has built in support for validating based on field length and several variations of field value, including regular expressions. Zip codes are pretty easy to express as a regular expression.

Code Block
languagejava
  
  @Validate("required,regexp=^\\d{5}(-\\d{4})?$")
  public String getZip()
  {

    public returnString zip;
  }

Let's give it a try; restart the application and enter an "abc" for the zip code.

Image Modified
Regexp validation

This is what you'll see after typing "abc" and tabbing out of the field, then tabbing back in. It's a little hard to capture all the animation effects in a still photoclicking the Create Address button.

Note

Modern browsers will automatically validate a regexp field when the form is submitted, as shown above. Older browsers do not have that automatic support, but will still validate input, using the same decorations as for the required fields in the previous screenshot.

In any case, that's the right validation behavior, but it's the wrong message. Your users are not going to know or care about regular expressions.

Fortunately, it's easy to customize validation messages. All we need to know is the name of the property ("zip") and the name of the validator ("regexp"). We can then put an entry into the CreateAddress message catalog:

No Format

zip-regexp-message=Zip Codes are five or nine digits.  Example: 02134 or 90125-1655.

Refresh the page and submit again:

Image Modified

Regexp validation with corrected message

This trick isn't limited to just the regexp validator, it works equally well with any validator.

Let's go one step further. Turns out, we can move the regexp pattern to the message catalog as well. If you only provide the name of the validator in the @Validate annotation, Tapestry will search the containing page's message catalog of the constraint value, as well as the validation message. The constraint value for the regexp validator is the regular expression to match against.

Code Block
languagejava
  
  @Validate("required,regexp")
    public String getZip()
  {
    return zip;
  }

Now, just put the regular expression into the CreateAddress message catalog:

No Format

zip-regexp=^\\d{5}(-\\d{4})?$
zip-regexp-message=Zip Codes are five or nine digits.  Example: 02134 or 90125-1655.

After a restart you'll see the ... the same behavior. But when we start creating more complicated regular expressions, it'll be much, much nicer to put them in the message catalog rather than inside the annotation value. And inside the message catalog, you can change and tweak the regular expressions without having to restart the application each time.

...

By now you are likely curious about what happens after the form submits successfully (without validation errors), so that's what we'll focus on next.

Next: Using Tapestry With Hibernate

 

Scrollbar
Continue on to Chapter 5: Forms in Tapestry, Part Two