apache > cocoon
 

Ajax

Introduction

Ajax is a catchy name for using JavaScript client-side code that communicates asynchronously with the server, thus allowing to escape the traditional web model where each button click translates to a full page reload.

Cocoon Forms has a number of features that allow the building of highly-interactive forms, such as validation, value-change listeners, intra-form actions, repeaters, etc. Having a full page reload for each action doesn't provide a good user experience and puts an unncessary load on the server.

Starting with Cocoon 2.1.8, Cocoon Forms offers (mostly) transparent Ajax support. What this means is that when a form is submitted, this happens in the background and only those parts of the form that actually changed are updated on the page. Full-page reload only happens when interaction with the form is finished.

How to activate Ajax on your forms?

The "ajax" attribute

Activating Ajax mode on your forms is straightforward: just add ajax="true" on the <ft:form-template> element in your form template:

<ft:form-template action="continue" method="POST" ajax="true">
  <ft:widget id="email"/>
  .../...
</ft:form-template>

There are a number of things to take care of though for the Ajax mode to function properly.

Use the Template Generator

As of version 2.1.8, Ajax support is implemented only in the JXTemplate version of the Forms Template language. This means that the form display pipeline must ust the JXTemplate generator and use jx-macros.xml:

<html xmlns:ft="http://apache.org/cocoon/forms/1.0#template"
      xmlns:jx="http://apache.org/cocoon/templates/jx/1.0">

  <!-- Import the macros that define CForms template elements -->
  <jx:import uri="resource://org/apache/cocoon/forms/generation/jx-macros.xml"/>
  <body>
    <ft:form-template action="continue" method="POST" ajax="true">
    ...

Container widgets must enclose a container element

The Ajax support of Cocoon Forms works by sending partial page updates to the browser. This requires the representation of widgets to be contained in a single element, which becomes the "replacement unit" for that widget.

The styling stylesheets take care of this for terminal widgets such as fields and actions, but not for container widgets whose layout are completely defined by the page template. Concretely, this means that <ft:repeater>, <ft:group> and <ft:union> instructions must contain a single element such as HTML <div>, <table>, <span>, etc.

<ft:group id="info">
  <div>
    <p><ft:widget id="foo"/></p>
    <p><ft:widget id="bar"/></p>
  </div>
</ft:group>

The formatting pipeline with then take care of adding an id attribute on the container element so that it can be found and updated in the displayed page.

Use <ft:repeater> instead of <ft:repeater-widget>

A problem with the existing <ft:repeater-widget> instruction is that it handles two different kinds of widgets, the repeater itself and its rows. This has the drawback of not allowing individual refresh of rows and does not allow the template system to identify a unique element that contains the repeater, as explained above.

Starting with Cocoon 2.1.8, the template for repeaters involve two instructions, <ft:repeater> and <ft:repeater-rows>:

  • <ft:repeater> starts the repeater template, and sets the repeater as the context widget. This instruction must contain a single element.
  • <ft:repeater-rows> iterates through all rows in the repeater. Again, and because a row is a container widget, this instruction must contain a single element. Note that <ft:repeater-rows> has to be a descendant of <ft:repeater>, but does not need to be a direct child and that any number of document elements can enclose <ft:repeater-rows> (not form templates instruction though).

Remember also that the JXTemplate macros define the repeater and repeaterLoop variables in <ft:repeater> and <ft:repeater-rows> respectively that allow for some powerful conditional templates.

Here's a complete example, which is what you will have in many real-world situations:

Here are your contacts:

<ft:repeater id="contacts">
  <jx:choose>

    <!-- formatting of an empty "contacts" -->
    <jx:when test="${repeater.size == 0}">
      <span>
        You have no contacts.
        <ft:widget id="../add-contact"/>
      </span>
    </jx:when>

    <!-- formatting for a non-empty "contacts" -->
    <jx:otherwise>
      <div>
        <table>
          <tr>
            <th>First name</th><th>Last name</th><th>Email</th>
          </tr>
          <ft:repeater-rows>
            <!-- container element for rows -->
            <tr class="row-${repeaterLoop.index % 2}"> 
              <ft:widget id="firstname"/>
              <ft:widget id="lastname"/>
              <ft:widget id="email"/>
              <ft:widget id="select"/>
            </tr>
          </ft:repeater-rows>
        </table>
        <ft:widget id="../add-contact"/>
        <ft:widget id="../delete-contact"/>
      </div>
    </jx:otherwise>

  </jx:choose>
</ft:repeater>

<ft:widget id="ok"/>

This example shows some conditional formatting (using JXTemplate) depending if the repeater is empty or not. In the two cases, the action widgets that allow to remove and delete rows are added within the repeater template. We use the "../add-contact" notation ("../" goes back to the parent widget) as these actions are defined as siblings of the repeater in the form definition, but we want them to be included in the repeater's conditional templates. We therefore need to "escape" the repeater to lookup its siblings.

Notice the <span> element when the repeater is empty. Even it we don't really need it from the page layout point of view, it is required to have a single element for Ajax updates when the user adds a contact.

Notice also the <div> element when the repeater is not empty. We cannot use the <table> that encloses rows, as again, we want to include the "add-contact" and "delete-contacts" actions in the conditional template. We therefore need an enclosing element for the table and the actions.

The <ft:repeater-rows> doesn't need anything special as the <td> serves as the container. The class="row-${repeaterLoop.index % 2}" allows the generation of alternating colors of rows, as even rows will have class="row-0" and odd rows will have class="row-1".

Using Ajax in portals and aggregations: the "ajax-action" attribute

Portals do a lot of URL rewriting to ensure that only aggregated portal pages are displayed, even if individual portlets use their own URLs without caring about being integrating in a portal. This rewriting defeats the Ajax system that posts the form in the background using the <form>'s action attribute, which is rewritten by the portal.

To allow the Ajax system to function in a portal, you must add an additional ajax-action attribute on <ft:form-template>. When present, this attribute's value is where Ajax requests will be posted instead of the value of action.

<ft:form-template action="aggregation" ajax-action="continue-form">
  .../...
</ft:form-template>

Note that because of the portal's URL rewriting, the two attributes will often have the same values in the template.

The ajax-action attribute must also be used when the form page is produced by an aggregation (e.g. with <map:aggregate>): the form's action attribute will point to the full aggregation, which is not suitable to produce page update fragments. The ajax-action attribute must then point to the URL that handles for form and not to the full aggregation.

The form display pipeline for Ajax

Using Ajax in your forms requires a special pipeline: along with the regular form display pipeline, we need additional components that will filter the form when an Ajax request is processed, so that only partial page update instructions are sent to the browser, and not the full HTML page.

Here's a typical Ajax form display pipeline:

<map:match pattern="viewform-*">
  <map:generate type="jx" src="pages/{1}.xml"/>
  <map:transform type="browser-update"/>
  <map:transform type="i18n"/>
  <map:transform src="resources/forms-styling.xsl"/>
  <map:transform type="i18n">
  <map:select type="ajax-request">
    <map:when test="true">
      <map:serialize type="xml"/>
    </map:when>
    <map:otherwise>
      <map:serialize type="html"/>
    </map:otherwise>
  </map:select>
</map:match>

Things to notice in this pipeline:

  • the JXTemplate generator is used. Ajax is not yet implemented in the FormsTransformer
  • the "browser-update" transformer filters the result of the template, so that only updated widgets are sent back to the browser
  • the serializer depends on the request type: an Ajax requests must return XML, whereas non-Ajax requests can return HTML.

The additional components are declared as follows in the sitemap:

<map:components>
  <map:transformers default="xslt">    
    <map:transformer name="browser-update" src="org.apache.cocoon.ajax.BrowserUpdateTransformer"/>
  </map:transformers>
  <map:selectors>
    <map:selector name="ajax-request" src="org.apache.cocoon.ajax.AjaxRequestSelector"/>
  </map:selectors>
</map:components>

Debugging Ajax forms

Debugging Ajax forms is less easy than regular forms with full page reloads, as some processing occurs in the background. When an error occurs in a background resquest, the client-side JavaScript library prompts the user to display the server response in a separate window. Popup windows must be allowed for the target server for this to function properly.

Another invaluable tool is the little-know TCP monitor utility hidden inside Axis. This is a small Java application that acts as a HTTP tunnel or a proxy that sits inbetween the browser and the server, and records all requests and responses, which can then be examined.

Other useful tools are the classical TCP sniffers such as the Unix tcpdump command or the Ethereal tool. They are not easy to setup and master, though.

Additional information

You can also find additional information in the Ajax presentation from the Cocoon GetTogether 2005.