Ajax Development Basics

The overall concept behind the majority of built in AJAX functionality in Tapestry is that everything should work exactly the same way in your pages/components in AJAX requests as it does during normal page rendering. There are a few corner cases that have no real intuitive XHR equivalent which we will discuss as well.

Including JavaScript in your HTML Template

One of the first things you will need to do is make sure you have the necessary javascript includes. Tapestry makes extensive use of the Dojo javascript toolkit and provides a couple of options for including it for you automatically via the Shell or ScriptIncludes components. An example of defining a basic outer layer using the Shell component would look like:

<html jwcid="@Shell" title="Basic Ajax Page">
<body jwcid="@Body">

    <p>Basic javascript inclusion sample.</p>

</body>
</html>

Updating Components: The universal updateComponents parameter

One of the most frequently used pieces of ajax functionality is the updateComponents="foo,bar" parameter that is now implemented by all of the core Tapestry components - where it makes sense. Some of the more familiar of these are DirectLink, LinkSubmit, Form, ImageSubmit and Submit. We'll build on our previous example and use the DirectLink component to refresh a current time display.

..
<p>Basic javascript inclusion sample.</p>

<p>
    <a jwcid="@DirectLink" listener="listener:onTime" updateComponents="time">Refresh time</a>.
</p>

<div jwcid="time@Insert" value="ognl:time" renderTag="true" />
..

The corresponding java page class:

..
public abstract BasicAjax extends BasePage {

        @InitialValue("new java.util.Date()")
    public abstract void setTime(Date time);

    public void onTime()
    {
        setTime(new java.util.Date());
    }
}

That's it! Building and running this small example should be all you need to start using the very basic AJAX functionality provided by Tapestry.

updateComponents == IComponent.getClientId()

There are a few subtle things happening here that may not be apparent at first glance. One of the more important changes in Tapestry 4.1.x was the addition of the IComponent.getClientId() method to all component classes. When dealing with client side javascript functionality the unique client side element id of the html elements your components generate becomes very important. What this method gives you is the unique element id attribute that will be written for any given component depending on the context in which you call it.

If you are in a For loop then the clientId will automatically be incremented for each loop and always represent the unique value of the component. The same semantics work whether in Forms / For or any other nested type of structure. This has been one of key changes in the API that has made the AJAX functionality provided much easier to work with.

In our example the updateComponents parameter given made use of the new smart auto binding functionality added to the framework recently, but you could write the equivalent parameter using an ognl expression as well:

<a jwcid="@DirectLink" listener="listener:setTime"
    updateComponents="ognl:components.time.clientId">Refresh time</a>

Things can get a lot uglier once you start trying to update multiple components:

<a jwcid="@DirectLink" listener="listener:setTime"
    updateComponents="ognl:{components.time.clientId, page.components.status.clientId}">Refresh time</a>

The much easier simple string list updateComponents="compA, compB, compC" is not only shorter / easier to understand but also more efficient and robust in many different circumstances. For instance, the auto binding functionality would be able to detect that you were trying to update a component contained within one of your own custom components as well as a component on the page containing your component and wire up and find all the correct clientId values for you automatically.

The thing to walk away with here is that in 98% of the cases you run in to this style of updateComponents syntax is the way to go:

<a jwcid="@DirectLink" listener="listener:setTime" updateComponents="time,status">Refresh time</a>

How clientId values are generated

The actual value that is output by a component is determined by a number of rules depending on the context and type of component involved. Of course, no id will be generated at all if your custom components don't call the new AbstractComponent.renderIdAttribute(IMarkupWriter, IRequestCycle) method provided in the base AbstractComponent superclass that most components derive from. When that method is called the rules for determining what value to render out in to the generated html element are evaluated in this order:

  1. informal id parameter bound? - If the definition of the component being rendered had an informal id="UpdatedComponent" parameter specified that will take higher precendence than anything else. It may eventually end up being output as id="UpdatedComponent_4" if your component is being rendered in a loop but it will still use the id as the basis for its client side id values.
    <div jwcid="@Any" id="customId" >Content</div>
  2. use IComponent.getId() value - If no informal id parameter is bound then the next candidate used to seed the clientIds is the value returned from IComponent.getId(). This will be the id specified in your Page.page file or the id assigned in your html snippet as in:
    <div jwcid="customId@Any">Content</div>
    
    or .page file:
    
    <component id="customId" type="Any">
    </component>
  3. form component? - If the component in question implements IFormComponent then the same semantics as above apply with the caveat that the inner workings of client id generation work slightly differently here as they are synchronized back with the normal Form input name/clientId semantics that Tapestry has always had.

The previously mentioned renderIdAttribute method is the crucual piece that anyone writing custom components will need to make sure to call if you plan on overriding IComponent.renderComponent. One very simplistic example of calling this new base method would be:

..
public abstract MyCustomComponent extends AbstractComponent {

    public void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
    {
        writer.begin("div");

        writer.renderIdAttribute(writer, cycle); <----------------------------------*
        writer.renderInformalParamters(writer, cycle);

        writer.print("Custom text content..");

        writer.end();
    }
}

Common Mistake: Can't update a component not already rendered on the page

One of more common questions on the users mailing list inevitably involves this innocent looking block of code modified from our previous examples:

..
<p>Basic javascript inclusion sample.</p>

<p>
    <a jwcid="@DirectLink" listener="listener:setTime" updateComponents="time">Refresh time</a>.
</p>

<span jwcid="@If" condition="ognl:time != null">
    <div jwcid="time@Insert" value="ognl:time" renderTag="true" />
</span>
..

The problem with this code is that the component being requested to update - time - doesn't actually exist as a valid client side html element when you click on the Refresh Time link. The initial block of html generated from this template will really look like:

..
<p>Basic javascript inclusion sample.</p>

<p>
    <a href="http://localhost:80080/app?..." id="DirectLink"
        onClick="return tapestry.linkOnClick(this.href)">Refresh time</a>.
</p>
..

The crucial piece missing in the above sample snippet is an html block for our time component. It wasn't rendered initially because the time value was null. The semantics of how the client side XHR io logic works basically comes down to:

  • make XHR IO Request - Initiate client side XHR request which will eventually return a block of html for our time component of:
    <div id="time">10:49 am 1/2/2007</div>
  • find the matching client side dom node and update it - This is the part that breaks down. The very basic minimal example of how it is done would look something like:
    ..
    var responseNode = getResponseHtml(); // this is the time div block from the last example
    
    var updateNode = document.getElementById("time");
    updateNode.innerHTML = getContentAsString(responseNode);
    ..

    The resulting client side error that is logged should be something along the lines of "couldn't find a matching client side dom node to update with id of 'time'".

    The solution to get around this varies for each situation but the most common / easiest method is usually done by just wrapping the block that conditionally renders with a simple Any component:

    ..
    <p>Basic javascript inclusion sample.</p>
    
    <p>
        <a jwcid="@DirectLink" listener="listener:setTime" updateComponents="updateArea">Refresh time</a>.
    </p>
    
    <span jwcid="updateArea@Any">
        <span jwcid="@If" condition="ognl:time != null">
            <div jwcid="time@Insert" value="ognl:time" renderTag="true" />
        </span>
    </span>
    ..

    Now the example specifies a updateComponents="updateArea" definition for which components to update and has the Any updated instead of time directly as it will exist on the client side prior to our requesting an update on it and shouldn't interfere overly much with whatever CSS/design we have setup.