AJAX Filter CRUD

This example is based on the Filter CRUD example and adds AJAX functionality with Tapestry's Zone component.
Highlight zone updates? This shows you which zones are updated by the Ajax response.

Create...
 
References: @ActivationRequestParameter.

Home

The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.
The source for CustomForm and CustomError is shown in the No Validation Bubbles example.

Persons.tml


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">
<head>
    <link rel="stylesheet" type="text/css" href="${context:css/together/ajaxfiltercrud.css}"/>
</head>
<body>
    <h1>AJAX Filter CRUD</h1>
    
    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     

    This example is based on the <em>Filter CRUD</em> example and adds AJAX functionality with Tapestry's Zone component.<br/>
    
    <div class="eg">
        <form t:type="form" t:id="preferencesForm">
            Highlight zone updates? 
             <input t:type="checkbox" t:id="highlightZoneUpdates" value="highlightZoneUpdates" onclick="this.form.submit()"/>
            This shows you which zones are updated by the Ajax response.
        </form><br/>
    
        <a t:type="eventLink" t:event="toCreate" t:zone="editorZone" href="#">Create...</a><br/>
        
        <table id="listAndEditor">
            <tbody>
                <tr>
    
                    <!-- This is the left side of the table: a list of Persons -->
    
                    <td id="listCell">
                        <t:zone t:id="listZone" id="listZone" t:update="prop:zoneUpdateFunction">
        
                            <!-- We can't use the form's id in the css because the Zone messes with it, so we add a div with its own id. -->
                            <div id="personFilter">
                                <form t:type="form" t:id="filterForm" t:zone="listZone">
                                    <div>
                                        Person
                                    </div>
                                    <div>
                                        <input t:id="partialName" t:type="TextField" size="15" t:validate="maxLength=15"/>
                                        <input type="submit" value="Filter" title="Filter"/>
                                    </div>
                                </form>
                            </div>
                            
                            <div id="personList">
                                <table t:type="grid" t:id="list" t:source="listPersons" t:row="listPerson"
                                    t:exclude="id,version,firstName,lastName,region,startDate" t:add="name"
                                    t:rowsPerPage="4" t:pagerPosition="bottom"
                                    t:class="personGrid" t:empty="block:emptyPersons" t:inPlace="true">[Grid here]
                                    <p:nameCell>
                                        <a t:type="eventLink" t:event="selected" t:context="listPerson.id" class="prop:linkCSSClass" t:zone="^" href="#">
                                            ${listPerson.firstName} ${listPerson.lastName}
                                        </a>
                                    </p:nameCell>
                                </table>
                            </div>
    
                            <t:block t:id="emptyPersons">
                                <div id="noPersons">
                                    (No persons found)
                                </div>
                            </t:block>
                            
                        </t:zone>
                    </td>
                    
                    <!-- This is the right side of the table: where a Person will be created, reviewed, or updated. -->
            
                    <td id="editorCell">
                        <t:zone t:id="editorZone" id="editorZone" t:update="prop:zoneUpdateFunction">
        
                            <t:if test="modeCreate">
                                <h1>Create</h1>
                                
                                <form t:type="CustomForm" t:id="createForm" t:zone="^" >
                                    <t:errors/>
                            
                                    <table>
                                        <tr>
                                            <th><t:label for="firstName"/>:</th>
                                            <td><input t:type="TextField" t:id="firstName" value="editorPerson.firstName" t:validate="required, maxlength=10" size="10"/></td>
                                            <td>(required)</td>
                                        </tr>
                                        <tr class="err">
                                            <th></th>
                                            <td colspan="2"><t:CustomError for="firstName"/></td>
                                        </tr>
                                        <tr>
                                            <th><t:label for="lastName"/>:</th>
                                            <td><input t:type="TextField" t:id="lastName" t:clientid="clastname" value="editorPerson.lastName" t:validate="required, maxlength=10" size="10"/></td>
                                            <td>(required)</td>
                                        </tr>
                                        <tr class="err">
                                            <th></th>
                                            <td colspan="2"><t:CustomError for="lastName"/></td>
                                        </tr>
                                        <tr>
                                            <th><t:label for="region"/>:</th>
                                            <td><input t:type="Select" t:id="region" value="editorPerson.region" t:validate="required" t:blankOption="ALWAYS"/></td>
                                            <td>(required)</td>
                                        </tr>
                                        <tr class="err">
                                            <th></th>
                                            <td colspan="2"><t:CustomError for="region"/></td>
                                        </tr>
                                        <tr>
                                            <th><t:label for="startDate"/>:</th>
                                            <td><input t:type="DateField" t:id="startDate" t:clientid="cstartdate" value="editorPerson.startDate" t:format="prop:dateFormat" t:validate="required" size="10"/></td>
                                            <td>(required, ${datePattern})</td>
                                        </tr>
                                        <tr class="err">
                                            <th></th>
                                            <td colspan="2"><t:CustomError for="startDate"/></td>
                                        </tr>
                                    </table>
        
                                    <div class="buttons">
                                        <a t:type="eventLink" t:event="cancelCreate" t:zone="^" href="#">Cancel</a>
                                        <input type="submit" value="Save"/>
                                    </div>
                                </form>
        
                            </t:if>
        
                            <t:if test="modeReview">
                                <h1>Review</h1>
                                
                                <t:if test="editorPerson">
                                    <div t:type="if" t:test="deleteMessage" class="error">
                                        ${deleteMessage}
                                    </div>
        
                                    <table>
                                        <tr>
                                            <th>Id:</th>
                                            <td>${editorPerson.id}</td>
                                        </tr>
                                        <tr>
                                            <th>Version:</th>
                                            <td>${editorPerson.version}</td>
                                        </tr>
                                        <tr>
                                            <th>Name:</th>
                                            <td>${editorPerson.firstName} ${editorPerson.lastName}</td>
                                        </tr>
                                        <tr>
                                            <th>Region:</th>
                                            <td>${editorPersonRegion}</td>
                                        </tr>
                                        <tr>
                                            <th>Start Date:</th>
                                            <td><t:output value="editorPerson.startDate" format="prop:dateFormat"/></td>
                                        </tr>
                                    </table>
        
                                    <div class="buttons">
                                        <a t:type="eventLink" t:event="toUpdate" t:context="editorPerson.id" t:zone="^" href="#">Update...</a>
                                        <a t:type="eventLink" t:event="delete" t:context="[editorPerson.id,editorPerson.version]" t:zone="^" href="#">
                                            <!-- The Confirm mixin can't cancel an EventLink that specifies a Zone, so we put the Confirm inside the EventLink. -->
                                            <!-- See http://tapestry-users.832.n2.nabble.com/Confirm-mixin-won-t-cancel-when-in-zone-td5048950.html#a5048950 -->
                                            <span t:type="any" t:mixins="Confirm" t:message="Delete ${editorPerson.firstName} ${editorPerson.lastName}?">
                                                Delete...
                                            </span>
                                        </a>
                                    </div>
        
                                </t:if>
                                <t:if negate="true" test="editorPerson">
                                    Person ${editorPersonId} does not exist.<br/><br/>
                                </t:if>
                                
                            </t:if>
        
                            <t:if test="modeUpdate">
                                <h1>Update</h1>
                                
                                <form t:type="CustomForm" t:id="updateForm" t:context="editorPersonId" t:zone="^">
                                    <t:errors/>
                            
                                    <t:if test="editorPerson">
                                        <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
                                        <t:hidden value="editorPerson.version"/>
                                
                                        <table>
                                            <tr>
                                                <th><t:label for="updFirstName"/>:</th>
                                                <td><input t:type="TextField" t:id="updFirstName" value="editorPerson.firstName" t:validate="required, maxlength=10" size="10"/></td>
                                                <td>(required)</td>
                                            </tr>
                                            <tr class="err">
                                                <th></th>
                                                <td colspan="2"><t:CustomError for="updFirstName"/></td>
                                            </tr>
                                            <tr>
                                                <th><t:label for="updLastName"/>:</th>
                                                <td><input t:type="TextField" t:id="updLastName" value="editorPerson.lastName" t:validate="required, maxlength=10" size="10"/></td>
                                                <td>(required)</td>
                                            </tr>
                                            <tr class="err">
                                                <th></th>
                                                <td colspan="2"><t:CustomError for="updLastName"/></td>
                                            </tr>
                                            <tr>
                                                <th><t:label for="updRegion"/>:</th>
                                                <td><input t:type="Select" t:id="updRegion" value="editorPerson.region" t:validate="required"/></td>
                                                <td>(required)</td>
                                            </tr>
                                            <tr class="err">
                                                <th></th>
                                                <td colspan="2"><t:CustomError for="updRegion"/></td>
                                            </tr>
                                            <tr>
                                                <th><t:label for="updStartDate"/>:</th>
                                                <td><input t:type="DateField" t:id="updStartDate" value="editorPerson.startDate" t:format="prop:dateFormat" t:validate="required" size="10"/></td>
                                                <td>(required, ${datePattern})</td>
                                            </tr>
                                            <tr class="err">
                                                <th></th>
                                                <td colspan="2"><t:CustomError for="updStartDate"/></td>
                                            </tr>
                                        </table>
        
                                        <div class="buttons">
                                            <a t:type="eventLink" t:event="cancelUpdate" t:context="editorPersonId" t:zone="^" href="#">Cancel</a>
                                            <input t:type="submit" value="Save"/>
                                        </div>
                                    </t:if>
                                    <t:if negate="true" test="editorPerson">
                                        Person ${editorPersonId} does not exist.<br/><br/>
                                    </t:if>
                                        
                                </form>
                                
                            </t:if>
    
                            <!-- This is needed to clear the zone. -->
                            <t:if test="modeNull">
                                <!-- The space character is needed only to make the zone update highlight visible. -->
                                &nbsp;
                            </t:if>
    
                        </t:zone> 
                    </td>
                    
                </tr>
            </tbody>
        </table>
    </div>
    
    References: 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/annotations/ActivationRequestParameter.html">@ActivationRequestParameter</a>.<br/><br/>
 
    <a t:type="pageLink" t:page="Index" href="#">Home</a><br/><br/>
    
    The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.<br/>
    The source for CustomForm and CustomError is shown in the No Validation Bubbles example.<br/><br/>

    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/ajaxfiltercrud/Persons.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/ajaxfiltercrud/Persons.properties"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/ajaxfiltercrud/Persons.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/together/ajaxfiltercrud.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/Confirm.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/Confirm.js"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/model/together/PersonFilteredDataSource.java"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/Person.java"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/Regions.java"/>
</body>
</html>

Persons.properties


region-blankLabel=Choose...

## These enum conversions could be moved to the central message properties file called app.properties
Regions.EAST_COAST=East Coast
Regions.WEST_COAST=West Coast

updFirstName-label=First Name
updLastName-label=Last Name
updRegion-label=Region
updStartDate-label=Start Date

Persons.java


package jumpstart.web.pages.together.ajaxfiltercrud;

import java.text.Format;
import java.text.SimpleDateFormat;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.Regions;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.util.ExceptionUtil;
import jumpstart.web.components.CustomForm;
import jumpstart.web.model.together.PersonFilteredDataSource;

import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.ActivationRequestParameter;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

public class Persons {

    private final String demoModeStr = System.getProperty("jumpstart.demo-mode");

    private enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Screen fields

    @Property
    @Persist
    private boolean highlightZoneUpdates;

    @Property
    // If we use @ActivationRequestParameter instead of @Persist, then our handler for filter form success would have
    // to render more than just the listZone, it would have to render all other links and forms: it would need a zone
    // around the "Create..." link so it could render it; and it would render the editorZone, which would be destructive
    // if the user has been typing into Create or Update. Alternatively, it could use a custom JavaScript callback to
    // update the partialName in all other links and forms - see AjaxResponseRenderer#addCallback(JavaScriptCallback).
    @Persist
    private String partialName;

    @Property
    private Person listPerson;

    @Property
    @ActivationRequestParameter
    private Mode editorMode;

    @Property
    @ActivationRequestParameter
    private Long editorPersonId;

    @Property
    private Person editorPerson;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String deleteMessage;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    @Component
    private CustomForm createForm;

    @Component
    private CustomForm updateForm;

    @InjectComponent
    private Zone listZone;

    @InjectComponent
    private Zone editorZone;

    @Inject
    private Messages messages;

    @Inject
    private Request request;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;

    // The code

    // /////////////////////////////////////////////////////////////////////
    // FILTER
    // /////////////////////////////////////////////////////////////////////

    void onSuccessFromFilterForm() {
        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // CREATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toCreate"

    void onToCreate() {
        editorMode = Mode.CREATE;
        editorPersonId = null;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    // Handle event "cancelCreate"

    void onCancelCreate() {
        editorMode = null;
        editorPersonId = null;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Component "createForm" bubbles up the PREPARE event when it is rendered or submitted

    void onPrepareFromCreateForm() throws Exception {
        editorMode = Mode.CREATE;
        // Instantiate a Person for the form data to overlay.
        editorPerson = new Person();
    }

    // Component "createForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromCreateForm() {

        if (createForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        if (demoModeStr != null && demoModeStr.equals("true")) {
            createForm.recordError("Sorry, but Create is not allowed in Demo mode.");
            return;
        }

        try {
            editorPerson = personManagerService.createPerson(editorPerson);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            createForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "createForm" bubbles up SUCCESS or FAILURE when it is submitted, depending on whether VALIDATE
    // records an error

    void onSuccessFromCreateForm() {
        editorMode = Mode.REVIEW;
        editorPersonId = editorPerson.getId();

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    void onFailureFromCreateForm() {
        editorMode = Mode.CREATE;
        editorPersonId = null;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // REVIEW
    // /////////////////////////////////////////////////////////////////////

    // Handle event "selected"

    void onSelected(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;

        if (request.isXHR()) {
            editorPerson = personFinderService.findPerson(personId);
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // UPDATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toUpdate"

    void onToUpdate(Long personId) {
        editorMode = Mode.UPDATE;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Handle event "cancelUpdate"

    void onCancelUpdate(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;

        if (request.isXHR()) {
            editorPerson = personFinderService.findPerson(personId);
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Component "updateForm" bubbles up the PREPARE_FOR_RENDER event when it is rendered

    void onPrepareForRenderFromUpdateForm(Long personId) {
        editorMode = Mode.UPDATE;
        editorPersonId = personId;

        // If the form is valid then we're not redisplaying due to error, so get the editorPerson.

        if (updateForm.isValid()) {
            editorPerson = personFinderService.findPerson(personId);
            // Handle null editorPerson in the template.
        }
    }

    // Component "updateForm" bubbles up the PREPARE_FOR_SUBMIT event when it is submitted

    void onPrepareForSubmitFromUpdateForm(Long personId) {
        editorPersonId = personId;

        // Get objects for the form fields to overlay.
        editorPerson = personFinderService.findPerson(personId);

        if (editorPerson == null) {
            editorPerson = new Person();
            updateForm.recordError("Person has been deleted by another process.");
        }
    }

    // Component "updateForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromUpdateForm() {

        if (updateForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        try {
            personManagerService.changePerson(editorPerson);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            updateForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "updateForm" bubbles up SUCCESS or FAILURE when it is submitted, depending on whether VALIDATE
    // records an error

    void onSuccessFromUpdateForm() {
        editorMode = Mode.REVIEW;

        if (request.isXHR()) {
            editorPerson = personFinderService.findPerson(editorPersonId);
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    void onFailureFromUpdateForm() {
        editorMode = Mode.UPDATE;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // DELETE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "delete"

    void onDelete(Long personId, Integer personVersion) {

        editorPersonId = personId;
        boolean successfulDelete = false;

        if (demoModeStr != null && demoModeStr.equals("true")) {
            deleteMessage = "Sorry, but Delete is not allowed in Demo mode.";
        }
        else {

            try {
                personManagerService.deletePerson(personId, personVersion);
                successfulDelete = true;
            }
            catch (Exception e) {
                // Display the cause. In a real system we would try harder to get a user-friendly message.
                deleteMessage = ExceptionUtil.getRootCauseMessage(e);
            }

        }

        if (successfulDelete) {
            editorMode = null;
            editorPersonId = null;

            if (request.isXHR()) {
                ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
            }
        }
        else {
            editorMode = Mode.REVIEW;
            editorPersonId = personId;

            if (request.isXHR()) {
                editorPerson = personFinderService.findPerson(personId);
                ajaxResponseRenderer.addRender(editorZone);
            }
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // OTHER
    // /////////////////////////////////////////////////////////////////////

    public GridDataSource getListPersons() {
        return new PersonFilteredDataSource(personFinderService, partialName);
    }

    public String getLinkCSSClass() {
        if (listPerson != null && listPerson.getId().equals(editorPersonId)) {
            return "active";
        }
        else {
            return "";
        }
    }

    public String getZoneUpdateFunction() {
        return highlightZoneUpdates ? "highlight" : "show";
    }

    public boolean isModeCreate() {
        return editorMode == Mode.CREATE;
    }

    public boolean isModeReview() {
        return editorMode == Mode.REVIEW;
    }

    public boolean isModeUpdate() {
        return editorMode == Mode.UPDATE;
    }

    public boolean isModeNull() {
        return editorMode == null;
    }

    public String getEditorPersonRegion() {
        // Follow the same naming convention that the Select component uses
        return messages.get(Regions.class.getSimpleName() + "." + editorPerson.getRegion().name());
    }

    public String getDatePattern() {
        return "dd/MM/yyyy";
    }

    public Format getDateFormat() {
        return new SimpleDateFormat(getDatePattern());
    }
}

ajaxfiltercrud.css


body, td        { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333;
                    line-height: 17px; }

.eg             { margin: 20px 0; padding: 20px; 
                    border: 1px solid #ddd; border-radius: 4px; -webkit-border-radius: 4px; -mox-border-radius: 4px; }

a               { text-decoration: none; color: #3D69B6; }
a:hover         { text-decoration: underline; }

.js-required    { color: red; display: block; margin-bottom: 14px; }

table       { border-collapse: collapse; border-spacing: 0; }
:root table { border-collapse: separate; } /* Firefox 3 */
th, td      { padding: 0; }
form        { margin-bottom: 0; } /* IE 7 */

#listAndEditor          { width: 800px; border: none; margin: 10px 0; }

#listCell               { width: 25%; border: 1px solid white; background-color: #eee; vertical-align: top; }

#personFilter           { width: 100%; padding: 5px 0 10px 0; text-align: center; vertical-align: middle; 
                            background-color: #3d69b6; color: white; font-weight: bold; border: 1px solid white; }

/* Add some padding around the list so that we can see the yellow flash when "Highlight zone updates" is on. */ 
#listZone               { padding: 4px; background-color: inherit; }
#listZone               { background-color: #eee; zoom: 1; } /* IE 7. zoom is to fix hasLayout bug. */

#personList             { height: 238px; position: relative; }
.personGrid             { width: 100%; font-family: Arial, Helvetica, sans-serif; }
.personGrid th          { display: none; }
.personGrid td          { border: thin solid white; background-color: #eee; }
.personGrid a           { width: 100%; line-height: 50px; display: block; text-align: center;
                            text-decoration: none; color: black; }
.personGrid a:visited   { color: inherit; }
.personGrid a:hover     { background: #ccc; color: #fff; }
.personGrid a.active    { background: #999; color: #fff; }
.personGrid span.current    { background-color: #3d69b6; }

#personList .t-data-grid-pager  /* Need line-height to work around IE7 hasLayout and missing margins bug */
                        { line-height: 24px; position: absolute; left: 2px; bottom: 0px; margin: 0;
                            font-family: Arial, Helvetica, sans-serif; }

#noPersons              { text-align: center; padding-top: 10px; }

#editorCell             { width: 75%; height: 100%; vertical-align: top; 
                             border: 1px solid white; background-color: #eee; padding: 20px; }
#editorZone             { background-color: inherit; /* For IE7: */ background-color: #eee; }
#editorCell table       { margin: auto; } 
#editorCell h1          { font-size: large; text-align: center; } 
#editorCell th          { padding: 2px 5px; text-align: right; }
#editorCell td          { padding: 2px 5px; text-align: left; }
#editorCell tr.err th   { padding: 0; }
#editorCell tr.err td   { padding: 0; }
#editorCell tr.err td.error-msg-c   { padding: 0 0 4px 7px; font-size: 11px; color: red; } 
#editorCell .buttons    { text-align: center; padding-top: 15px; } 

.error                  { color: red; text-align: center; padding-bottom: 13px; }

Confirm.java


// Based on http://wiki.apache.org/tapestry/Tapestry5AndJavaScriptExplained

package jumpstart.web.mixins;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.annotations.AfterRender;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

/**
 * A simple mixin for attaching a javascript confirmation box to the onclick event of any component that implements
 * ClientElement.
 * 
 * @author <a href="mailto:chris@thegodcode.net">Chris Lewis</a> Apr 18, 2008
 */
// The @Import tells Tapestry to put a link to the file in the head of the page so that the browser will pull it in. 
@Import(library = "Confirm.js")
public class Confirm {

    @Parameter(name = "message", value = "Are you sure?", defaultPrefix = BindingConstants.LITERAL)
    private String message;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    @InjectContainer
    private ClientElement clientElement;

    @AfterRender
    public void afterRender() {

        // Tell the Tapestry.Initializer to do the initializing of a Confirm, which it will do when the DOM has been
        // fully loaded.

        JSONObject spec = new JSONObject();
        spec.put("elementId", clientElement.getClientId());
        spec.put("message", message);
        javaScriptSupport.addInitializerCall("confirm", spec);
    }

}

Confirm.js


// Based on http://wiki.apache.org/tapestry/Tapestry5AndJavaScriptExplained

// A class that attaches a confirmation box (with logic)  to
// the 'onclick' event of any HTML element.
// @author Chris Lewis Apr 18, 2008 <chris@thegodcode.net>

Confirm = Class.create({
        
    initialize: function(elementId, message) {
        this.message = message;
        Event.observe($(elementId), 'click', this.doConfirm.bindAsEventListener(this));
    },
    
    doConfirm: function(e) {
        
        // Pop up a javascript Confirm Box (see http://www.w3schools.com/js/js_popup.asp)
        
        if (!confirm(this.message)) {
                e.stop();
        }
    }
        
})

// Extend the Tapestry.Initializer with a static method that instantiates a Confirm.

Tapestry.Initializer.confirm = function(spec) {
    new Confirm(spec.elementId, spec.message);
}

PersonFilteredDataSource.java


package jumpstart.web.model.together;

import java.util.List;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceRemote;

import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.grid.SortConstraint;

public class PersonFilteredDataSource implements GridDataSource {
    private IPersonFinderServiceRemote personFinderService;
    private String partialName;

    private int startIndex;
    private List<Person> preparedResults;

    public PersonFilteredDataSource(IPersonFinderServiceRemote personFinderService, String partialName) {
        this.personFinderService = personFinderService;
        this.partialName = partialName;
    }

    @Override
    public int getAvailableRows() {
        return (int) personFinderService.countPersons(partialName);
    }

    @Override
    public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {
        preparedResults = personFinderService.findPersons(partialName, startIndex, endIndex - startIndex + 1);
        this.startIndex = startIndex;
    }

    @Override
    public Object getRowValue(final int index) {
        return preparedResults.get(index - startIndex);
    }

    @Override
    public Class<Person> getRowType() {
        return Person.class;
    }

}

Person.java


package jumpstart.business.domain.person;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;


/**
 * The Person entity.
 */
@Entity
@SuppressWarnings("serial")
public class Person implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false)
    private Long id;

    @Version
    @Column(nullable = false)
    private Integer version;

    @Column(length = 10, nullable = false)
    private String firstName;

    @Column(length = 10, nullable = false)
    private String lastName;
    
    @Enumerated(EnumType.STRING)
    private Regions region;

    @Temporal(TemporalType.DATE)
    private Date startDate;

    public String toString() {
        final String DIVIDER = ", ";
        
        StringBuilder buf = new StringBuilder();
        buf.append(this.getClass().getSimpleName() + ": ");
        buf.append("[");
        buf.append("id=" + id + DIVIDER);
        buf.append("version=" + version + DIVIDER);
        buf.append("firstName=" + firstName + DIVIDER);
        buf.append("lastName=" + lastName + DIVIDER);
        buf.append("region=" + region + DIVIDER);
        buf.append("startDate=" + startDate);
        buf.append("]");
        return buf.toString();
    }

    // Default constructor is required by EJB3.
    public Person() {
    }

    public Person(String firstName, String lastName, Regions region, Date startDate) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
        this.region = region;
        this.startDate = startDate;
    }

    // The need for an equals() method is discussed at http://www.hibernate.org/109.html
    
    @Override
    public boolean equals(Object obj) {
        return (obj == this) || (obj instanceof Person) && id != null && id.equals(((Person) obj).getId());
    }

    // The need for a hashCode() method is discussed at http://www.hibernate.org/109.html

    @Override
    public int hashCode() {
        return id == null ? super.hashCode() : id.hashCode();
    }

    @PrePersist
    @PreUpdate
    public void validate() throws ValidationException {

        // Validate syntax...

        if ((firstName == null) || (firstName.trim().length() == 0)) {
            throw new ValidationException("First name is required.");
        }

        if ((lastName == null) || (lastName.trim().length() == 0)) {
            throw new ValidationException("Last name is required.");
        }

        if (region == null) {
            throw new ValidationException("Region is required.");
        }

        if (startDate == null) {
            throw new ValidationException("Start date is required.");
        }

    }

    public Long getId() {
        return id;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

    public String getFirstName() {
        return firstName;
    }

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

    public String getLastName() {
        return lastName;
    }

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

    public Regions getRegion() {
        return region;
    }

    public void setRegion(Regions region) {
        this.region = region;
    }

    public Date getStartDate() {
        return startDate;
    }

    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

}

Regions.java


package jumpstart.business.domain.person;

public enum Regions {
    EAST_COAST, WEST_COAST;
}