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.
Review |
<!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
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
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
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
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
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
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;
}