|
||||||
---|---|---|---|---|---|---|
<!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">
<body class="container">
<h3>Ajax Forms in a Loop (With Deleted Entity Handling)</h3>
<noscript class="js-required">
${message:javascript_required}
</noscript>
This example is a little more complex, adding logic to gracefully recover from choosing Edit, Save, or Cancel on an entity that no longer exists,<br/>
so it suits applications that use "hard" delete (ie. that actually delete the entity rather than updating it to a "deleted" state).<br/><br/>
To demonstrate a server-side error, change any First Name to <em>${BAD_NAME}</em>.
<div class="eg">
<table id="personsTable" class="table table-striped table-hover table-condensed">
<thead>
<tr>
<th>
<table class="inner">
<thead>
<tr>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Region</th>
<th>Start Date</th>
<th>Action</th>
</tr>
</thead>
</table>
</th>
</tr>
</thead>
<tbody>
<t:Loop t:source="persons" t:value="person">
<tr t:type="zone" t:id="rowZone" id="prop:currentRowZoneId">
<td>
<t:form t:id="personForm" context="person.id" async="true" validate="person">
<table class="inner">
<tbody>
<t:if test="person.id">
<tr>
<td>
<span t:type="any">${person.id}</span>
<!-- If optimistic locking is not needed then comment out this next line. -->
<t:hidden value="person.version"/>
</td>
<td>
<t:if test="!editing">
${person.firstName}
</t:if>
<t:if test="editing">
<t:textfield t:id="firstName" value="person.firstName"/>
</t:if>
</td>
<td>
${person.lastName}
<!-- We shadow each output-only with a hidden field to enable redisplay of the list exactly as it was submitted. -->
<t:hidden value="person.lastName"/>
</td>
<td>
<t:if test="!editing">
${personRegion}
</t:if>
<t:if test="editing">
<t:select t:id="region" value="person.region"/>
</t:if>
</td>
<td>
<t:if test="!editing">
<t:output t:value="person.startDate" t:format="prop:dateFormat"/>
</t:if>
<t:if test="editing">
<t:datefield t:id="startDate" value="person.startDate" format="prop:dateFormat"/>
</t:if>
</td>
<td>
<t:if test="!editing">
<t:eventlink event="toEdit" context="person.id" async="true" class="btn btn-default">Edit</t:eventlink>
</t:if>
<t:if test="editing">
<t:submit t:id="save" value="Save"/>
<t:eventlink event="cancel" context="person.id" async="true" class="btn btn-default">Cancel</t:eventlink>
</t:if>
</td>
</tr>
</t:if>
<t:if test="personFormHasErrors">
<tr>
<td colspan="6">
<t:errors globalOnly="true"/>
</td>
</tr>
</t:if>
</tbody>
</table>
</t:form>
</td>
</tr>
</t:Loop>
</tbody>
</table>
</div>
Notes:
<ul>
<li>Not tested with IE7 or earlier.</li>
</ul>
References:
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Loop.html">Loop</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Zone.html">Zone</a>,
<a href="http://tapestry.apache.org/ajax-and-zones.html">Ajax and Zones</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/EventLink.html">EventLink</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/Request.html">Request</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/ajax/AjaxResponseRenderer.html">AjaxResponseRenderer</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ioc/annotations/Inject.html">@Inject</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/InjectComponent.html">@InjectComponent</a>,
<a href="http://tapestry.apache.org/5.4/coffeescript/zone.html">t5/core/zone</a>,
<a href="http://tapestry.apache.org/5.4/coffeescript/ajax.html">t5/core/ajax</a>,
<a href="http://tapestry.apache.org/5.4/coffeescript/forms.html">t5/core/forms</a>.<br/><br/>
<t:pagelink page="Index">Home</t:pagelink><br/><br/>
The source for IPersonFinderServiceLocal, IPersonManagerServiceLocal, 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/examples/ajax/AjaxFormsInALoopWithDEH.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormsInALoopWithDEH.java"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormsInALoopWithDEH.properties"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/ajaxformsinaloop.css"/>
</t:tabgroup>
</body>
</html>
package jumpstart.web.pages.examples.ajax;
import java.text.DateFormat;
import java.text.Format;
import java.util.List;
import java.util.Locale;
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 org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.Zone;
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;
@Import(stylesheet = "css/examples/ajaxformsinaloop.css")
public class AjaxFormsInALoopWithDEH {
static private final int MAX_RESULTS = 30;
// Screen fields
@Property
private List<Person> persons;
@Property
private Person person;
@Property
private boolean editing;
@Property
private final String BAD_NAME = "Acme";
// Work fields
private boolean loadingLoop;
private Long personId;
// Generally useful bits and pieces
@EJB
private IPersonFinderServiceLocal personFinderService;
@EJB
private IPersonManagerServiceLocal personManagerService;
@InjectComponent
private Zone rowZone;
@Inject
private Request request;
@Inject
private AjaxResponseRenderer ajaxResponseRenderer;
@InjectComponent
private Form personForm;
@Inject
private Messages messages;
@Inject
private Locale currentLocale;
// The code
void onActivate() {
loadingLoop = false;
}
void setupRender() {
loadingLoop = true;
// Get all persons - ask business service to find them (from the database)
persons = personFinderService.findPersons(MAX_RESULTS);
}
void onPrepareForRenderFromPersonForm(Long personId) {
// If the loop is being reloaded, the form may have had errors so clear them just in case.
if (loadingLoop) {
personForm.clearErrors();
editing = false;
}
// If the form is valid then we're not redisplaying due to error, so get the person.
if (personForm.isValid()) {
person = personFinderService.findPerson(personId);
// Handle null person in the template.
}
}
void onPrepareForSubmitFromPersonForm(Long personId) {
this.personId = personId;
// Get objects for the form fields to overlay.
person = personFinderService.findPerson(personId);
if (person == null) {
// Create an empty person for the form fields to overlay. It avoids NPE.
person = new Person();
personForm.recordError("Person has been deleted by another process.");
}
}
void onValidateFromPersonForm() {
// Simulate a server-side validation error: return error if anyone's first name is BAD_NAME.
if (person.getFirstName() != null && person.getFirstName().equals(BAD_NAME)) {
personForm.recordError("First name must not be " + BAD_NAME + ".");
}
if (person.getId() == 2 && !person.getFirstName().equals("Mary")) {
personForm.recordError("First name for this person must be Mary.");
}
if (personForm.getHasErrors()) {
// We get here only if a server-side validator detected an error.
return;
}
try {
personManagerService.changePerson(person);
}
catch (Exception e) {
// Display the cause. In a real system we would try harder to get a user-friendly message.
personForm.recordError(ExceptionUtil.getRootCauseMessage(e));
}
}
void onSuccessFromPersonForm() {
editing = false;
if (request.isXHR()) {
ajaxResponseRenderer.addRender(rowZone);
}
}
void onFailureFromPersonForm() {
editing = true;
if (request.isXHR()) {
ajaxResponseRenderer.addRender(rowZone);
}
}
void onToEdit(Long personId) {
this.personId = personId;
person = personFinderService.findPerson(personId);
editing = true;
if (request.isXHR()) {
ajaxResponseRenderer.addRender(rowZone);
}
}
void onCancel(Long personId) {
this.personId = personId;
person = personFinderService.findPerson(personId);
editing = false;
if (request.isXHR()) {
ajaxResponseRenderer.addRender(rowZone);
}
}
public String getCurrentRowZoneId() {
// The id attribute of a row must be the same every time that row asks for it and unique on the page.
return "rowZone_" + getPersonId();
}
public Long getPersonId() {
// Here we ensure we return the person's id even if they have been deleted since the page was loaded.
return loadingLoop ? person.getId() : personId;
}
public String getPersonRegion() {
return messages.get(Regions.class.getSimpleName() + "." + person.getRegion().name());
}
public Format getDateFormat() {
return DateFormat.getDateInstance(DateFormat.SHORT, currentLocale);
}
public boolean isPersonFormHasErrors() {
return personForm.getHasErrors();
}
}
## 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
.eg {
margin: 20px 0;
padding: 14px;
border: 1px solid #ddd;
border-radius: 6px;
-webkit-border-radius: 6px;
-mox-border-radius: 6px;
}
.inner {
border-collapse: collapse;
border-spacing: 0;
font-size: inherit;
width: 100%;
}
.inner>thead>tr>th {
padding: 3px 5px;
width: 130px;
text-align: left;
}
.inner>tbody>tr>td {
padding: 3px 5px;
width: 130px;
text-align: left;
}
.inner div.alert h4 {
display: none;
}
.inner div.alert ul {
list-style: none;
}
.inner div.alert li {
text-align: center;
}
.js-required {
color: red;
display: block;
margin-bottom: 14px;
}