|
||||||
|---|---|---|---|---|---|---|
<!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_3.xsd">
<head>
<link rel="stylesheet" type="text/css" href="${context:css/examples/ajaxformsinaloop.css}"/>
</head>
<body>
<h1>Ajax Forms in a Loop</h1>
<noscript class="js-required">
${message:javascript_required}
</noscript>
To demonstrate a server-side error, change any First Name to <em>${BAD_NAME}</em>.
<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. <br/>
Note that it upsets our hover highlighting due to <a href="https://issues.apache.org/jira/browse/TAP5-2014">TAP5-2014</a>.
</form><br/>
<table id="personsTable" class="outer">
<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" class="prop:evenodd.next" t:update="prop:zoneUpdateFunction">
<td>
<form t:id="personForm" t:type="form" t:context="person.id" t:zone="^" t:clientValidation="false">
<table class="inner">
<tbody>
<tr>
<td>
<span t:type="any" id="personId">${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">
<input t:type="TextField" t:id="firstName" value="person.firstName" t:validate="required, maxlength=10" size="10"/>
</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">
<input t:type="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">
<input t:type="DateField" t:id="startDate" t:value="person.startDate" t:format="prop:dateFormat" size="10"/>
</t:if>
</td>
<td>
<t:if test="!editing">
<input t:type="submit" t:id="edit" value="Edit" t:mode="cancel"/>
</t:if>
<t:if test="editing">
<input t:type="submit" t:id="save" value="Save"/>
<input t:type="submit" t:id="cancel" value="Cancel" t:mode="cancel"/>
</t:if>
</td>
</tr>
<t:if test="personFormHasErrors">
<tr>
<td colspan="6">
<t:errors/>
</td>
</tr>
</t:if>
</tbody>
</table>
</form>
</td>
</tr>
</t:Loop>
</tbody>
</table>
</div>
Here's the sequence when you Edit, Save, or Cancel:
<ul>
<li>In comes an AJAX component event request: a submit from a Form.</li>
<li>- Form bubbles up <em>prepareForSubmit</em>. It has a context.</li>
<li>- Submit bubbles up <em>selected</em>.</li>
<li>- Form bubbles up <em>validate</em>.</li>
<li>- Form bubbles up <em>success</em> or <em>failure</em>. Our event handlers nominate which Zone to render.</li>
<li>Tapestry renders the Zone...</li>
<li>- Form bubbles up <em>prepareForRender</em>. It has a context.</li>
<li>Out goes an AJAX response with Zone content which includes the Form.</li>
</ul>
Notes:
<ul>
<li>This example does not cope with choosing Edit, Save, or Cancel on an entity that no longer exists,<br/>
so it suits applications that use "soft" delete (ie. that update the entity to a "deleted" state rather than actually deleting it).</li>
<li>Not tested with IE7 or earlier.</li>
</ul>
References:
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/Loop.html">Loop</a>,
<a href="http://tapestry.apache.org/5.3.7/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.3.7/apidocs/org/apache/tapestry5/corelib/components/EventLink.html">EventLink</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/services/Request.html">Request</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/ioc/annotations/Inject.html">@Inject</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/annotations/InjectComponent.html">@InjectComponent</a>.<br/><br/>
<a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>
The source for Person, PersonFinderService, @EJB handling, etc. is shown in the @EJB example.<br/><br/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormsInALoop.tml"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormsInALoop.java"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormsInALoop.properties"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/ajaxformsinaloop.css"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/commons/EvenOdd.java"/>
</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 jumpstart.web.commons.EvenOdd;
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.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;
public class AjaxFormsInALoop {
static private final int MAX_RESULTS = 30;
// Screen fields
@Property
private List<Person> persons;
@Property
private Person person;
@Property
private boolean editing;
@Property
private EvenOdd evenOdd;
@Property
private final String BAD_NAME = "Acme";
@Property
@Persist
private boolean highlightZoneUpdates;
// Work fields
private boolean loadingLoop;
private Actions action;
// 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;
@Component
private Form personForm;
@Inject
private Messages messages;
@Inject
private Locale currentLocale;
private enum Actions {
TO_EDIT, CANCEL, SAVE;
}
// 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);
evenOdd = new EvenOdd();
}
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) {
// Get objects for the form fields to overlay.
person = personFinderService.findPerson(personId);
}
void onSelectedFromEdit() {
action = Actions.TO_EDIT;
}
void onSelectedFromSave() {
action = Actions.SAVE;
}
void onSelectedFromCancel() {
action = Actions.CANCEL;
}
void onValidateFromPersonForm() {
if (action == Actions.SAVE) {
if (personForm.getHasErrors()) {
// We get here only if a server-side validator detected an error.
return;
}
// 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 cannot be " + BAD_NAME + ".");
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));
}
}
else {
personForm.clearErrors();
}
}
void onSuccessFromPersonForm() {
editing = action == Actions.TO_EDIT;
if (request.isXHR()) {
ajaxResponseRenderer.addRender(rowZone);
}
}
void onFailureFromPersonForm() {
editing = action == Actions.SAVE;
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_" + person.getId();
}
public String getPersonRegion() {
// Follow the same naming convention that the Select component uses
return messages.get(Regions.class.getSimpleName() + "." + person.getRegion().name());
}
public Format getDateFormat() {
return DateFormat.getDateInstance(DateFormat.SHORT, currentLocale);
}
public boolean isPersonFormHasErrors() {
return personForm.getHasErrors();
}
public String getZoneUpdateFunction() {
return highlightZoneUpdates ? "highlight" : "show";
}
}
## 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
body, th, td { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333;
line-height: 17px; }
h1 { font-size: 26px; line-height: 20px; } /* For IE 7 */
.eg { margin: 20px 0; padding: 20px; color: #888;
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; }
/* We'll use very specific selectors to avoid interfering with the DatePicker. */
.outer { border-collapse: collapse; border-spacing: 0; border: 1px solid #dddddd; font-size: inherit; }
.outer > tbody > tr.odd { background-color: #f8f8f8; }
.outer > tbody > tr:hover { background-color: #eeeeee; }
.outer > tbody > tr.odd:hover { background-color: #eeeeee; }
.outer > tbody > tr > td > form { margin: 0; padding: 0; }
.inner { border-collapse: collapse; border-spacing: 0; font-size: inherit; width: 100%; }
.inner > thead > tr > th { padding: 3px 5px; text-align: left; width: 130px; border: 1px solid #dddddd;
font-weight: normal; background-color: #eeeeee;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbfbfb', endColorstr='#e4e4e4'); /* for IE */
background: -webkit-gradient(linear, left top, left bottom, from(#fbfbfb), to(#e4e4e4)); /* for webkit browsers */
background: -moz-linear-gradient(top, #fbfbfb, #e4e4e4); /* for firefox 3.6+ */ }
.inner > tbody > tr > td { padding: 3px 5px; width: 130px; height: 2.3em; text-align:left; }
.inner div.t-error { margin: 0; border: none; }
.inner div.t-error div.t-banner { display: none; }
.inner div.t-error ul { color: inherit; background-color: inherit; list-style: none; margin: 0; }
.inner div.t-error li { color: red; background-color: inherit; text-align: center; }
.nodata { margin-top: 5px; margin-left:20px; }
.js-required { color: red; display: block; margin-bottom: 14px; }
// Adapted from Tapestry 4's EvenOdd class.
package jumpstart.web.commons;
/**
* Used to emit a stream of alterating string values: "even", "odd", etc. This is often used in the Inspector pages to
* make the class of a <tr> alternate for presentation reasons.
*
* @author Howard Lewis Ship
*/
public class EvenOdd {
private boolean even = true;
/**
* Returns "even" or "odd". Whatever it returns on one invocation, it will return the opposite on the next. By
* default, the first value returned is "even".
*/
public String getNext() {
String result = getCurrent();
even = !even;
return result;
}
public String getCurrent() {
return even ? "even" : "odd";
}
public boolean isEven() {
return even;
}
/**
* Overrides the even flag.
*/
public void setEven(boolean value) {
even = value;
}
}