AJAX On Event: the ZoneUpdater Mixin

This page demonstrates a custom mixin called ZoneUpdater that enables a server-side listener method to update a zone in response to a client-side event.
Welcome Humpty Dumpty.

Note that the following time field does not update because it's not in the zone: Wed Nov 22 04:12:00 EST 2017
For those using Tapestry5-jQuery, it provides equivalent functionality with its Bind mixin.

References: Inge's Zone Updater, Ajax and Zones, Zone, @RequestParameter, Request, t5/core/zone.

Home


<!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 On Event: the ZoneUpdater Mixin</h3>
    
    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     

    This page demonstrates a custom mixin called ZoneUpdater that enables a server-side listener method 
    to update a zone in response to a client-side event.
    
    <div class="eg">
        <t:form class="form-horizontal">
            <div class="form-group">
                <t:label for="firstName" class="col-sm-2"/>
                <div class="col-sm-4">
                    <t:textfield t:id="firstName" t:mixins="zoneUpdater" 
                        ZoneUpdater.clientEvent="keyup" ZoneUpdater.event="firstNameChanged" ZoneUpdater.zone="nameZone"/>
                </div>
            </div>
            <div class="form-group">
                <t:label for="lastName" class="col-sm-2"/>
                <div class="col-sm-4">
                    <t:textfield t:id="lastName" t:mixins="zoneUpdater" 
                        ZoneUpdater.clientEvent="keyup" ZoneUpdater.event="lastNameChanged" ZoneUpdater.zone="nameZone"/>
                </div>
            </div>
    
            <t:zone t:id="nameZone" id="nameZone">
                Welcome ${name}.
            </t:zone><br/>
    
            Note that the following time field does not update because it's not in the zone:  ${serverTime}<br/>
        </t:form>
    </div>
    
    For those using Tapestry5-jQuery, it provides equivalent functionality with its <em>Bind</em> mixin.<br/><br/>
    
    References: 
    <a href="http://tinybits.blogspot.com/2010/03/new-and-better-zoneupdater.html">Inge's Zone Updater</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/Zone.html">Zone</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/RequestParameter.html">@RequestParameter</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/coffeescript/zone.html">t5/core/zone</a>.<br/><br/>

    <t:eventlink event="gohome">Home</t:eventlink><br/><br/>

    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxOnEvent.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxOnEvent.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/js.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/mixins/ZoneUpdater.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/modules/zone-updater.js"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.examples.ajax;

import java.util.Date;

import jumpstart.web.pages.Index;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.RequestParameter;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

@Import(stylesheet = "css/examples/js.css")
public class AjaxOnEvent {

    // Screen fields

    @Property
    @Persist
    private String firstName;

    @Property
    @Persist
    private String lastName;

    // Generally useful bits and pieces

    @InjectComponent
    private Zone nameZone;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;

    @Inject
    private Request request;

    @Inject
    private ComponentResources componentResources;

    // The code

    // Life-cycle stuff. Fields that are marked @Persist MUST be initialized here rather than where they are declared.

    void setupRender() {
        if (firstName == null && lastName == null) {
            firstName = "Humpty";
            lastName = "Dumpty";
        }
    }

    void onFirstNameChanged(@RequestParameter(value = "param", allowBlank = true) String firstName) {
        if (firstName == null) {
            firstName = "";
        }

        this.firstName = firstName;

        if (request.isXHR()) {
            // Here we can do whatever updates we want, then return the content we want rendered.
            ajaxResponseRenderer.addRender(nameZone);
        }
    }

    void onLastNameChanged(@RequestParameter(value = "param", allowBlank = true) String lastName) {
        if (lastName == null) {
            lastName = "";
        }

        this.lastName = lastName;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(nameZone);
        }
    }

    public String getName() {
        return firstName + " " + lastName;
    }

    public Date getServerTime() {
        return new Date();
    }

    Object onGoHome() {
        componentResources.discardPersistentFieldChanges();
        return Index.class;
    }
}


.eg {
                margin: 20px 0;
                padding: 14px;
                color: #888;
                border: 1px solid #ddd;
                border-radius: 6px;
                -webkit-border-radius: 6px;
                -mox-border-radius: 6px;
}

.js-required {
                color: red;
                display: block;
                margin-bottom: 14px;
}

.js-recommended {
                color: red;
                display: block;
                margin-bottom: 14px;
}


/**
 * A simple mixin for attaching javascript that updates a zone on any client-side event.
 * Based on http://tinybits.blogspot.com/2010/03/new-and-better-zoneupdater.html
 */
package jumpstart.web.mixins;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

public class ZoneUpdater {

    // Parameters

    /**
     * The event to listen for on the client. If not specified, zone update can only be triggered manually through
     * calling updateZone on the JS object.
     */
    @Parameter(name = "clientEvent", defaultPrefix = BindingConstants.LITERAL)
    private String clientEvent;

    /**
     * The event to listen for in your component class
     */
    @Parameter(name = "event", defaultPrefix = BindingConstants.LITERAL, required = true)
    private String event;

    @Parameter(name = "prefix", defaultPrefix = BindingConstants.LITERAL, value = "default")
    private String prefix;

    @Parameter(name = "context")
    private Object[] context;

    /**
     * The zone to be updated by us.
     */
    @Parameter(name = "zone", defaultPrefix = BindingConstants.LITERAL, required = true)
    private String zone;

    /**
     * Set secure to true if https is being used, else set to false.
     */
    @Parameter(name = "secure", defaultPrefix = BindingConstants.LITERAL, value = "false")
    private boolean secure;

    // Useful bits and pieces

    @Inject
    private ComponentResources componentResources;

    @Environmental
    private JavaScriptSupport javaScriptSupport;

    /**
     * The element we attach ourselves to
     */
    @InjectContainer
    private ClientElement attachedTo;

    // The code

    void afterRender() {

        String listenerURI = componentResources.createEventLink(event, context).toAbsoluteURI(secure);

        javaScriptSupport.require("zone-updater").with(attachedTo.getClientId(), clientEvent, listenerURI, zone);
    }
}


// For an element (elementId), listens to it for a given event (clientEvent), and responds by issuing 
// an AJAX request (listenerURI) with the element value appended as a query string, 
// to update a zone (zoneId).
//
// Based on http://tinybits.blogspot.com/2010/03/new-and-better-zoneupdater.html
// and some help from Inge Solvoll.

define(["jquery", "t5/core/zone"], function($, zoneManager) {

    return function(elementId, clientEvent, listenerURI, zoneElementId) {
        var $element = $("#" + elementId);

        if (clientEvent) {
            $element.on(clientEvent, updateZone);
        }
    
        function updateZone() {
            var listenerURIWithValue = listenerURI;
    
            if ($element.val()) {
                listenerURIWithValue = appendQueryStringParameter(listenerURIWithValue, 'param', $element.val());
            }
    
            zoneManager.deferredZoneUpdate(zoneElementId, listenerURIWithValue);
        }
    }

    function appendQueryStringParameter(url, name, value) {
        if (url.indexOf('?') < 0) {
            url += '?'
        }
        else {
            url += '&';
        }
        value = escape(value);
        url += name + '=' + value;
        return url;
    }

});