Monday, 9 November 2009

Controlling Ajax in Tapestry

The standard way to do Ajax in Tapestry is with Zones, EventLinks and occasionally Blocks. This can result in a very simple TML file:



Hello ${name}!

Hello World!




Hello Sam


And an equally simple Java class:
@InjectComponent
private Zone helloZone;

@Property
private String name;

Object onHelloEvent(String name) {
this.name = name;
return helloZone.getBody();
}

public boolean isNamePopulated() {
return (name != null) && (name.length() > 0);
}

If that's all you need then job done (and not a line of JavaScript in sight!)

However, I'm finding that real world requirements often require greater control over the implementation details - typically this means we need to write our own JavaScript. Here's how we do it ...

First of all we modify the TML file to use a standard anchor in place of the EventLink:

Hello Sam


Now we need to tell JavaScript what to do with that link. But before we can start writing our JavaScript, we add the following to our Java class:
@Environmental
private RenderSupport renderSupport;

@Inject
private ComponentResources resources;

@Property
private String clientId;

void setupRender() {
clientId = renderSupport.allocateClientId(resources);
}

void afterRender() {
Link helloLink = resources.createEventLink("helloEvent", "Sam");
JSONArray parameters = new JSONArray();
parameters.put(clientId);
parameters.put(helloZone.getClientId());
parameters.put(helloLink.toAbsoluteURI());
renderSupport.addInit("initHello", parameters);
}

This makes use of two methods in Tapestry's rendering lifecycle - setupRender + afterRender. In setupRender we're getting unique client ID for this component. In afterRender we're passing some information to a JavaScript function via Tapestry's JSONArray object - note that one of these parameters is the URI of an EventLink we've created. For this to work, we also need to annotate the class:
@IncludeJavaScriptLibrary("myZone.js")

Finally we get to the JavaScript. What we need is a function that gets called when the page loads and accepts the three parameters we're passing in from the Java class. Here's what it looks like:
Tapestry.Initializer.initHello = function(element, zoneId, url) {
element = $(element);
$T(element).zoneId = zoneId;
element.observe("click", function(event) {
Event.stop(event);
var zoneObject = Tapestry.findZoneManager(element);
if (!zoneObject) return;
zoneObject.updateFromURL(url);
});
}

In fact, all that JavaScript was copied from the linkZone function in tapestry.js - we're doing exactly what Tapestry was doing, but now the JavaScript is under our control. :-)

So now we're in control of the JavaScript, what can we do with it? Pretty much anything we like as it turns out, but for now, let's pass the name in as a standard request parameter. First of all we modify our event handler to look like this:
@Inject
private Request request;

Object onHelloEvent() {
this.name = request.getParameter("name");
return helloZone.getBody();
}

And then we replace zoneObject.updateFromURL with a standard Prototype Ajax request:
// zoneObject.updateFromURL(url);
new Ajax.Request(url, {
method: 'get',
parameters: { "name" : "Sam" },
onException: Tapestry.ajaxFailureHandler,
onFailure: Tapestry.ajaxFailureHandler,
onSuccess : function (transport) {
zoneObject.processReply(transport.responseJSON);
}
});

Note that we are now using GET instead of POST and we have also exposed the onSuccess handler. This can prove very useful when we start implementing more advanced requirements.

1 comment:

Unknown said...

quite helpful, thanks. any chance of a multizone eg?