Smaller Components CRUD

Again, this example is based on the Filter CRUD example, but this time we've replaced the list and the editor with five custom components: PersonList, PersonCreate, PersonReview, PersonUpdate, and PersonEditor.

These smaller components are easier to read, easier to understand, and easier to maintain. And because each of our components fulfils a use case, the opportunity for mix-and-match reuse is higher.

Create...
Person
References: ComponentResourcesCommon, @ActivationRequestParameter, Environmental Services, FormSupport, ValidationTracker.

Home

The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.


<!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_4.xsd" xmlns:p="tapestry:parameter">
<body class="container">
    <h1>Smaller Components CRUD</h1>
    
    <p>Again, this example is based on the <em>Filter CRUD</em> example, but this time we've replaced the list and the editor with 
    <em>five</em> custom components: <em>PersonList</em>, <em>PersonCreate</em>, <em>PersonReview</em>, <em>PersonUpdate</em>, 
    and <em>PersonEditor</em>.</p>
    
    <p>These smaller components are easier to read, easier to understand, and easier to maintain. 
    And because each of our components fulfils a use case, the opportunity for mix-and-match reuse is higher.</p>

    <div class="eg">
        <t:eventLink event="toCreate">Create...</t:eventLink>
        
        <table id="listAndEditor">
            <tbody>
                <tr>
    
                    <!-- This is the left side of the table: a list of Persons -->
    
                    <td id="listSide">
                        <t:together.smallercomponentscrud.PersonList t:id="list" partialName="partialName" selectedPersonId="listPersonId"/>
                    </td>
                    
                    <!-- This is the right side of the table: where a Person will be created, reviewed, updated, or deleted. -->
            
                    <td id="editorSide">
                        <t:if test="isEditorMode('create')">
                            <t:together.smallercomponentscrud.PersonCreate t:id="personCreate"/>
                        </t:if>
                        <t:if test="isEditorMode('review')">
                            <t:together.smallercomponentscrud.PersonReview t:id="personReview" personId="editorPersonId"/>
                        </t:if>
                        <t:if test="isEditorMode('update')">
                            <t:together.smallercomponentscrud.PersonUpdate t:id="personUpdate" personId="editorPersonId"/>
                        </t:if>
                    </td>
                    
                </tr>
            </tbody>
        </table>
    </div>

    References: 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ComponentResourcesCommon.html">ComponentResourcesCommon</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/ActivationRequestParameter.html">@ActivationRequestParameter</a>, 
    <a href="http://tapestry.apache.org/environmental-services.html">Environmental Services</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/FormSupport.html">FormSupport</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ValidationTracker.html">ValidationTracker</a>.<br/><br/>

    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
    
    The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.<br/><br/>
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/together/smallercomponentscrud/Persons.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/together/smallercomponentscrud/Persons.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/together/filtercrud.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonList.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonList.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonCreate.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonCreate.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonReview.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonReview.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonUpdate.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonUpdate.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonEditor.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonEditor.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/smallercomponentscrud/PersonEditor.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/models/together/PersonFilteredDataSource.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/person/Person.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/person/Regions.java"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.together.smallercomponentscrud;

import jumpstart.business.domain.person.Person;

import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.annotations.ActivationRequestParameter;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.Property;

@Import(stylesheet = "css/together/filtercrud.css")
public class Persons {

    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // The activation context

    @Property
    private Mode editorMode;

    @Property
    private Long editorPersonId;

    // Screen fields

    @Property
    @ActivationRequestParameter
    private String partialName;

    @Property
    private Long listPersonId;

    // The code

    void onActivate(EventContext ec) {

        if (ec.getCount() == 0) {
            editorMode = null;
            editorPersonId = null;
        }
        else if (ec.getCount() == 1) {
            editorMode = ec.get(Mode.class, 0);
            editorPersonId = null;
        }
        else {
            editorMode = ec.get(Mode.class, 0);
            editorPersonId = ec.get(Long.class, 1);
        }

    }

    Object[] onPassivate() {

        if (editorMode == null) {
            return null;
        }
        else if (editorMode == Mode.CREATE) {
            return new Object[] { editorMode };
        }
        else if (editorMode == Mode.REVIEW || editorMode == Mode.UPDATE) {
            return new Object[] { editorMode, editorPersonId };
        }
        else {
            throw new IllegalStateException(editorMode.toString());
        }

    }

    void setupRender() {
        listPersonId = editorPersonId;
    }

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

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

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

    void onCanceledFromPersonCreate() {
        editorMode = null;
        editorPersonId = null;
    }

    void onCreatedFromPersonCreate(Person person) {
        editorMode = Mode.REVIEW;
        editorPersonId = person.getId();
    }

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

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

    void onDeletedFromPersonReview(Long personId) {
        editorMode = null;
        editorPersonId = null;
    }

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

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

    void onUpdatedFromPersonUpdate(Person person) {
        editorMode = Mode.REVIEW;
        editorPersonId = person.getId();
    }

    // /////////////////////////////////////////////////////////////////////
    // GETTERS ETC.
    // /////////////////////////////////////////////////////////////////////

    public boolean isEditorMode(Mode mode) {
        return editorMode == mode;
    }
}


.eg {
                margin: 20px 0;
                padding: 20px;
                color: #888;
                border: 1px solid #ddd;
                border-radius: 4px;
                -webkit-border-radius: 4px;
                -mox-border-radius: 4px;
                font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
                font-size: 13px;
                font-weight: normal;
                color: #333;
                line-height: 17px;
}

.eg a {
                text-decoration: none;
                color: #3D69B6;
}

.eg a:hover {
                text-decoration: underline;
}

#listAndEditor {
                width: 100%;
                max-width: 800px;
                border: none;
                margin: 10px 0 0;
}

#listSide {
                width: 25%;
                border-right: 3px solid white;
                vertical-align: top;
                padding: 0;
}

#listContainer {
                height: 328px;
                background-color: #eee;
}

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

#personFilter input[type="search"] {
                width: 90%;
                margin: 0 5%;
}

#personList {
                background-color: #eee;
}

#personList table {
                margin: 0;
}

#personList table th {
                display: none;
}

#personList table td {
                padding: 0;
                border: 0;
                border-bottom: 2px solid white;
                background-color: #eee;
}

#personList table a {
                width: 100%;
                line-height: 50px;
                display: block;
                text-align: center;
                text-decoration: none;
                color: black;
}

#personList table a:visited {
                color: inherit;
}

#personList table a:hover {
                background: #ccc;
                color: #fff;
                text-decoration: none;
}

#personList table a.active {
                background: #999;
                color: #fff;
}

#personList .pagination {
            margin: 9px 9px 7px;                
}

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

#editorSide {
                width: 75%;
                height: 100%;
                vertical-align: top;
                background-color: #eee;
                padding: 20px;
}

#editorSide h1 {
                font-size: large;
                text-align: center;
                margin: 0;
}

#editorSide > dl.well {
                background-color: transparent;
                border: none;
                box-shadow: none;
                margin-bottom: 0;
                padding: 19px;              
}

#editorSide form {
                padding-top: 19px;              
}

#editorSide .buttons {
                text-align: center;
}

.error {
                color: red;
                text-align: center;
                padding-top: 19px;
}

#editorSide form .error {
                padding-top: 0;
                padding-bottom: 19px;
}


<!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_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <div id="listContainer">

        <div id="personFilter">
            <t:form t:id="filterForm">
                <div>
                    Person
                </div>
                <div>
                    <t:textfield t:id="partialName" type="search" placeholder="Filter"/>
                    <t:submit value="Filter" title="Filter" style="display: none;"/>
                </div>
            </t:form>
        </div>
        
        <div id="personList">
            <t:grid t:id="list" source="persons" row="person" class="table"
                exclude="id,version,firstName,lastName,region,startDate" add="name"
                rowsPerPage="4" pagerPosition="bottom"
                empty="block:emptyPersons">
                <p:nameCell>
                    <t:eventLink event="personSelected" context="person.id" class="prop:linkCSSClass">
                        ${person.firstName} ${person.lastName}
                    </t:eventLink>
                </p:nameCell>
            </t:grid>
        </div>
        
        <t:block t:id="emptyPersons">
            <div id="noPersons">
                (No persons found)
            </div>
        </t:block>
    
    </div>

</t:content>
</html>


package jumpstart.web.components.together.smallercomponentscrud;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.web.models.together.PersonFilteredDataSource;

import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.grid.GridDataSource;

/**
 * This component will trigger the following events on its container (which in
 * this example is the page): {@link PersonList#PERSON_SELECTED}(Long personId).
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ PersonList.PERSON_SELECTED })
public class PersonList {
    public static final String PERSON_SELECTED = "personSelected";

    // Parameters

    @Parameter(required = true)
    @Property
    private String partialName;

    @Parameter(required = true)
    @Property
    private Long selectedPersonId;

    // Screen fields

    @Property
    private Person person;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    // The code

    boolean onPersonSelected(Long personId) {
        // Return false, which means we haven't handled the event so bubble it
        // up.
        // This method is here solely as documentation, because without this
        // method the event would bubble up anyway.
        return false;
    }

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

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


<!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_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <h1>Create</h1>
    
    <t:form t:id="form" class="form-horizontal" validate="person">
        <t:errors globalOnly="true"/>
        
        <t:together.smallercomponentscrud.PersonEditor person="person"/>

        <div class="form-group">
            <div class="col-sm-4 col-sm-offset-4">
                <t:submit value="Save" />
                <t:eventlink event="cancel" class="btn btn-default">Cancel</t:eventlink>
            </div>
        </div>
    </t:form>

</t:content>
</html>


package jumpstart.web.components.together.smallercomponentscrud;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.util.ExceptionUtil;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.ioc.annotations.Inject;

/**
 * This component will trigger the following events on its container (which in this example is the page):
 * {@link PersonCreate#CANCELED}, {@link PersonCreate#CREATED}(Long personId).
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ EventConstants.CANCELED, PersonCreate.CREATED })
public class PersonCreate {
    public static final String CANCELED = "canceled";
    public static final String CREATED = "created";

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

    // Parameters

    // Screen fields

    @Property
    private Person person;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    @InjectComponent
    private Form form;

    @Inject
    private ComponentResources componentResources;

    // The code

    boolean onCancel() {
        componentResources.triggerEvent(CANCELED, new Object[] {}, null);

        // We don't want the original to bubble up, so we return true to say
        // we've handled it.
        return true;
    }

    void onPrepareForRender() throws Exception {

        // If fresh start, make sure there's a Person object available.

        if (form.isValid()) {
            person = new Person();
        }
    }

    void onPrepareForSubmit() throws Exception {
        // Instantiate a Person for the form data to overlay.
        person = new Person();
    }

    boolean onValidateFromForm() {

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

        if (form.getHasErrors()) {
            return true;
        }

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

        return true;
    }

    boolean onSuccess() {
        // We want to tell our containing page explicitly what person we've created, so we trigger a new event with a
        // parameter. It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(CREATED, new Object[] { person }, null);

        // We don't want the original event to bubble up, so we return true to say we've handled it.
        return true;
    }

}


<!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_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <h1>Review</h1>
    
    <t:form t:id="form" class="form-horizontal">

        <t:if test="person">
            <div t:type="if" t:test="deleteMessage" class="error">
                ${deleteMessage}
            </div>
            
            <t:together.smallercomponentscrud.PersonEditor person="person" disabled="true"/>
    
            <div class="buttons">
                <t:eventlink event="toUpdate" context="person.id">Update...</t:eventlink>
                <t:eventlink event="delete" context="[person.id,person.version]" 
                    t:mixins="Confirm" Confirm.message="Delete ${person.firstName} ${person.lastName}?">Delete...</t:eventlink>
            </div>
        </t:if>
    
        <t:if test="!person">
            Person ${personId} does not exist.<br/><br/>
        </t:if>
        
    </t:form>
        
</t:content>
</html>


package jumpstart.web.components.together.smallercomponentscrud;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.util.ExceptionUtil;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;

/**
 * This component will trigger the following events on its container (which in
 * this example is the page): {@link PersonReview#TO_UPDATE}(Long personId),
 * {@link PersonReview#DELETED}(Long personId).
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ PersonReview.TO_UPDATE, PersonReview.DELETED })
public class PersonReview {
    public static final String TO_UPDATE = "toUpdate";
    public static final String DELETED = "deleted";

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

    // Parameters

    @Parameter(required = true)
    @Property
    private Long personId;

    // Screen fields

    @Property
    private Person person;

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

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    @Inject
    private ComponentResources componentResources;

    // The code

    void setupRender() {

        if (personId == null) {
            person = null;
            // Handle null person in the template.
        } else {
            if (person == null) {
                person = personFinderService.findPerson(personId);
                // Handle null person in the template.
            }
        }

    }

    boolean onToUpdate(Long personId) {
        // Return false, which means we haven't handled the event so bubble it
        // up.
        // This method is here solely as documentation, because without this
        // method the event would bubble up anyway.
        return false;
    }

    boolean onDelete(Long personId, Integer personVersion) {
        this.personId = personId;

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

            // We don't want the event to bubble up, so we return true to say
            // we've handled it.
            return true;
        }

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

            // We don't want the event to bubble up, so we return true to say
            // we've handled it.
            return true;
        }

        // Trigger new event which will bubble up.
        componentResources.triggerEvent(DELETED, new Object[] { personId },
                null);

        // We don't want the original event to bubble up, so we return true to
        // say we've handled it.
        return true;
    }

}


<!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_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <h1>Update</h1>
    
    <t:form t:id="form" class="form-horizontal" validate="person">
        <t:errors globalOnly="true"/>
    
        <t:if test="person">
            <!-- 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="person.version"/>
            
            <t:together.smallercomponentscrud.PersonEditor person="person"/>

            <div class="form-group">
                <div class="col-sm-4 col-sm-offset-4">
                    <t:submit value="Save" />
                    <t:eventlink event="cancel" context="personId" class="btn btn-default">Cancel</t:eventlink>
                </div>
            </div>
        </t:if>

        <t:if test="!person">
            Person ${personId} does not exist.<br/><br/>
        </t:if>
    </t:form>
                
</t:content>
</html>


package jumpstart.web.components.together.smallercomponentscrud;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.util.ExceptionUtil;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.ioc.annotations.Inject;

/**
 * This component will trigger the following events on its container (which in this example is the page):
 * {@link PersonUpdate#CANCELED}(Long personId), {@link PersonUpdate#UPDATED}(Long personId).
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ PersonUpdate.CANCELED, PersonUpdate.UPDATED })
public class PersonUpdate {
    public static final String CANCELED = "canceled";
    public static final String UPDATED = "updated";

    // Parameters

    @Parameter(required = true)
    @Property
    private Long personId;

    // Screen fields

    @Property
    private Person person;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    @InjectComponent
    private Form form;

    @Inject
    private ComponentResources componentResources;

    // The code

    boolean onCancel(Long personId) {
        this.personId = personId;

        componentResources.triggerEvent(CANCELED, new Object[] { personId }, null);
        // We don't want the original event to bubble up, so we return true to
        // say we've handled it.
        return true;
    }

    void onPrepareForRender() {

        // If fresh start, make sure there's a Person object available.

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

    void onPrepareForSubmit() {
        // Get objects for the form fields to overlay.
        person = personFinderService.findPerson(personId);

        if (person == null) {
            form.recordError("Person has been deleted by another process.");
            // Instantiate an empty person to avoid NPE in the Form.
            person = new Person();
        }
    }

    boolean onValidateFromForm() {

        if (form.getHasErrors()) {
            return true;
        }

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

        return true;
    }

    boolean onSuccess() {
        // We want to tell our containing page explicitly what person we've
        // updated, so we trigger a new event with a parameter. It will bubble
        // up because we don't have a handler method for it.
        componentResources.triggerEvent(UPDATED, new Object[] { person }, null);

        // We don't want the original event to bubble up, so we return true to
        // say we've handled it.
        return true;
    }

}


<!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_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <div class="form-group">
        <t:label for="firstName" class="col-sm-4"/>
        <div class="col-sm-4">
            <t:textfield t:id="firstName" value="person.firstName" disabled="disabled"/>
        </div>
        <div class="col-sm-4">
            <p class="form-control-static">(required)</p>
        </div>
    </div>
    <div class="form-group">
        <t:label for="lastName" class="col-sm-4"/>
        <div class="col-sm-4">
            <t:textfield t:id="lastName" value="person.lastName" disabled="disabled"/>
        </div>
        <div class="col-sm-4">
            <p class="form-control-static">(required)</p>
        </div>
    </div>
    <div class="form-group">
        <t:label for="region" class="col-sm-4"/>
        <div class="col-sm-4">
            <t:select t:id="region" value="person.region" blankOption="always" disabled="disabled"/>
        </div>
        <div class="col-sm-4">
            <p class="form-control-static">(required)</p>
        </div>
    </div>
    <div class="form-group">
        <t:label for="startDate" class="col-sm-4"/>
        <div class="col-sm-4">
            <t:datefield t:id="startDate" value="person.startDate" format="prop:dateFormat" disabled="disabled"/>
        </div>
        <div class="col-sm-4">
            <p class="form-control-static">(required, ${datePattern})</p>
        </div>
    </div>

</t:content>
</html>


region-blankLabel=Choose...

## These enum conversions could be moved to the central message properties file called app.properties
## The structure we've chosen (enum class name, dot, enum value) is the same as expected by the Select component.
Regions.EAST_COAST=East Coast
Regions.WEST_COAST=West Coast


package jumpstart.web.components.together.smallercomponentscrud;

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

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.Regions;

import org.apache.tapestry5.ComponentAction;
import org.apache.tapestry5.ValidationTracker;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.TextField;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.FormSupport;

public class PersonEditor {

    // Parameters

    @Parameter(required = true, allowNull = false)
    @Property
    private Person person;

    @Parameter(value = "false")
    @Property
    private boolean disabled;

    // Generally useful bits and pieces

    @InjectComponent("firstName")
    private TextField firstNameField;
    
    @Inject
    private Messages messages;

    @Environmental
    private FormSupport formSupport;

    @Environmental
    private ValidationTracker tracker;

    private static final ProcessSubmission PROCESS_SUBMISSION = new ProcessSubmission();

    // The code

    void afterRender() {

        // If we are inside a form, ask FormSupport to store PROCESS_SUBMISSION in its list of actions to do on submit.
        // If I contain other components, their actions will already be in the list, before PROCESS_SUBMISSION. That is
        // because this method, afterRender(), is late in the sequence. This guarantees PROCESS_SUBMISSION will be
        // executed on submit AFTER the components I contain are processed (which includes their validation).

        if (formSupport != null) {
            formSupport.store(this, PROCESS_SUBMISSION);
        }

    }

    private static class ProcessSubmission implements ComponentAction<PersonEditor> {
        private static final long serialVersionUID = 1L;

        public void execute(PersonEditor component) {
            component.processSubmission();
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName() + ".ProcessSubmission";
        }
    };

    private void processSubmission() {

        // Validate. We ensured in afterRender() that the components I contain have already been validated.

        if (person.getFirstName() != null && person.getFirstName().equals("Acme")) {
            tracker.recordError(firstNameField, firstNameField.getLabel() + " must not be Acme.");
        }

        if (person.getId() != null && person.getId() == 2 && !person.getFirstName().equals("Mary")) {
            tracker.recordError(firstNameField, firstNameField.getLabel() + " for this person must be Mary.");
        }

    }

    // ////////////////////////////////////////////////////////////////////////
    // GETTERS ETC.
    // ////////////////////////////////////////////////////////////////////////

    public String getPersonRegion() {
        return messages.get(Regions.class.getSimpleName() + "." + person.getRegion().name());
    }

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

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


package jumpstart.web.models.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;
    }

}


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;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;


/**
 * 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)
    @NotNull
    @Size(max = 10)
    private String firstName;

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

    @Temporal(TemporalType.DATE)
    @NotNull
    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 JPA.
    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 {

    }

    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;
    }

}


package jumpstart.business.domain.person;

public enum Regions {
    EAST_COAST, WEST_COAST;
}