Ajax Forms in a Loop (With Deleted Entity Handling)

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,
so it suits applications that use "hard" delete (ie. that actually delete the entity rather than updating it to a "deleted" state).

To demonstrate a server-side error, change any First Name to Acme.
Id First Name Last Name Region Start Date Action
5 87888Poop De scoop East Coast 7/7/07 Edit
1 Acme2 456 East Coast 6/19/52 Edit
2 Mary w big lamb West Coast 2/29/08 Edit
3 uyuy2 km West Coast 2/28/07 Edit
4 yuno 3clover West Coast 2/12/08 Edit
Notes: References: Loop, Zone, Ajax and Zones, EventLink, Request, AjaxResponseRenderer, @Inject, @InjectComponent, t5/core/zone, t5/core/ajax, t5/core/forms.

Home

The source for IPersonFinderServiceLocal, IPersonManagerServiceLocal, 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">
<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;
}