You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Next »

Tapestry Ajax Support

Tapestry includes sophisticated JavaScript and Ajax support, based on the Prototype and Scriptaculous libraries. These libraries are all packaged with Tapestry itself ... no extra download is required.

The goal for Tapestry is to have many basic and useful components available within the application itself, and make it easy for other JavaScript widgets to be encapsulated as Tapestry components.

Ajax support takes the form of new components and, occasionally, component mixins.

Changes to Prototype

Tapestry currently uses Prototype 1.6.0.3.

Changes to Scriptaculous

Scriptaculous normally has a special script loading option. Loading just the Scriptaculous main library, scriptaculous.js, will also load all the other scripts in the library. Normally, you can fine-tune this using "load" query parameter.

This doesn't fit well with the Tapestry; as discussed below, Tapestry has the capability to allow individual components to control which JavaScript libraries are loaded with the page. Further, the exact set of scripts needed is determined over the course of rendering the page, and depends on the needs of the specific components that have rendered.

The main Scriptaculous library, scriptaculous.js, is modified to turn off the autoloading behavior. Tapestry will automatically link in prototype.js, scriptaculous.js, effects.js and the Tapestry library, tapestry.js. You can add additional libraries as needed.

Basic JavaScript

The general strategy in Tapestry is that any significant amount of JavaScript should be packaged up as a static JavaScript library, a .js file that can be downloaded to the client.

Page specific JavaScript should be in the form of minimal statements to initialize objects, referencing the JavaScript libraries.

Most of this is accomplished via the [RenderSupport|../apidocs/org/apache/tapestry5/RenderSupport.html] object.

RenderSupport include a number of methods that will be used by components, or event by services that are called from components.

addScriptLink()

}}void addScriptLink(Asset... scriptAssets);{{

This method adds a link to a script file, a JavaScript library. A component can inject such a script and pass one or more of assets to this method. Tapestry will ensure that the necessary <link> elements are added to the top of the document (just inside the <head> element).

Adding the same asset multiple times does not create duplicate links. The subsequent ones are simply ignored. In this way, each component can add the assets it needs, without worrying about conflicts with other components.

Note that the Prototype, Scriptaculous main and effects libraries, and the standard Tapestry library (which largely consists of support for form input validation) are included automatically.

If you are need access to other Scriptaculous libraries, you can provide them as follows:

Unknown macro: {code|borderStyle=solid}

@Inject @Path("$

Unknown macro: {tapestry.scriptaculous}

/dragdrop.js")
private Asset dragDropLibrary;

@Environmental
private RenderSupport renderSupport;

void setupRender()

Unknown macro: { renderSupport.addScriptLink(dragDropLibrary); }

The Asset is injected, using the $

[symbol|../tapestry-ioc/symbols.html] to reference the location of the Scriptaculous library.

The RenderSupport is accessed as an Environmental service.

The setupRender() method (the name is specifically linked to a render phase) is the correct place to inform the RenderSupport service that the library is needed.

Even though the dragdrop.js library is stored inside a JAR file, Tapestry ensures that it can be accessed from the client web browser. A Tapestry URL within the virtual folder "/assets" is created; the file will be given a version number (the application version number if not specified more specifically) and will be sent to the browser with a far-future expires header (to encourage the browser to cache the file aggresively).

addScript()

}}void addScript(String format, Object... arguments);{{

This method adds some initialization JavaScript to the page. By initialization we mean that it goes at the bottom of the document, and will only be executed when the document has finished loading on the client (i.e., from the window.onload event handler).

When calling the method, the format string can include standard substitutions (such as '%s') for arguments. This saves you the trouble of calling String.format() yourself. In any case, the formatting JavaScript is added to the script block.

Injecting RenderSupport

RenderSupport is an environmental object, so you will normally inject it via the [Environmental|../apidocs/org/apache/tapestry5/annotations/Environmental.html] annotation:

@Environmental
private RenderSupport renderSupport;


Environmental only works inside components and occasionally a service may want to inject RenderSupport. Fortunately, a proxy of RenderSupport has been set up. The upshot of which is, you may also:

{code|borderStyle=solid}
  @Inject
  private RenderSupport renderSupport;

... or, in a service implementation constructor:

Unknown macro: {code|borderStyle=solid}

public MyServiceImpl(RenderSupport support)

Unknown macro: { . . . }

Inside a component, you should use Environmental, to highlight the fact that RenderSupport (like most environmental objects) is only available during rendering, not during action requests.

IncludeJavaScriptLibrary Annotation

The [IncludeJavaScriptLibrary|../apidocs/org/apache/tapestry5/annotations/IncludeJavaScriptLibrary.html] annotation is the easy way to include one or more JavaScript libraries.

The previous example could be re-written as:

@IncludeJavaScriptLibrary("$

Unknown macro: {tapestry.scriptaculous}

/dragdrop.js")
public class MyComponent

Unknown macro: { . . .}{code}

This saves you the effort of injecting the asset and making the call to RenderSupport. Chances are you will still inject RenderSupport so as to add some initialization JavaScript.

Combining Scripts

In production mode, Tapestry automatically combines JavaScript libraries. A single request (for a virtual asset) will retrieve the combined content of all referenced JavaScript library files.

This is a very useful feature, as it reduces the number of requests needed to present a page to the user.

As elsewhere, if the client browser supports gzip compression, the combined JavaScript will be compressed.

Ajax Components and Mixins

Autocomplete Mixin

The [Autocomplete|../apidocs/org/apache/tapestry5/corelib/mixins/Autocomplete.html] mixin exists to allow a text field to query the server for completions for a partially entered phrase. It is often used in situations where the field exists to select a single value from a large set, too large to succesfully download to the client as a drop down list; for example, when the number of values to select from is numbered in the thousands.

Autocomplete can be added to an existing text field:

Unknown macro: {code|borderStyle=solid}

<t:textfield t:id="accountName" t:mixins="autocomplete" size="100"/>


The mixin can be configured in a number of ways, see the [component reference|../tapestry-core/ref/].

When the user types into the field, the client-side JavaScript will send a request to the server to get completions.

You must write an event handler to provide these completions. The name of the event is "providecompletions". The context is the partial input value, and the return value will be converted into the selections for the user.

For example:

List<String> onProvideCompletionsFromAccountName(String partial)
{
List<Account> matches = accountDAO.findByPartialAccountName(partial);

List<String> result = new ArrayList<String>():

for (Account a : matches)

Unknown macro: { result.add(a.getName()); }

return result;
}


This presumes that }}findByPartialAccountName(){{ will sort the values, otherwise you will probably want to sort them. Certainly the Autocomplete mixin does _not_ do any sorting.

You can return an object array, a list, even a single object. You may return objects instead of strings ... and }}toString(){{ will be used to convert them into client-side strings.

Tapestry&apos;s default stylesheet includes entries for controlling the look of the floating popup of selections.

You may override }}DIV.t-autocomplete-menu UL{{ to change the main look and feel, }}DIV.t-autocomplete-menu LI{{ for a normal item in the popup list, and }}DIV.t-autocomplete-menu LI.selected{{ for the element under the cursor (or selecting using the arrow keys).

h2. Zone

Initial support for Zones is now in place. Zones are Tapestry&apos;s approach to performing partial updates to the client side. A Zone component renders as a &lt;div&gt; element with the &quot;t-zone&quot; CSS class (it can actually render as any element you chose, but for discussion&apos;s sake, we&apos;ll assume the &lt;div&gt; default). It also adds some JavaScript to the page to &quot;wire up&quot; a Tapestry.ZoneManager object to control updating the &lt;div&gt; element.

A Zone can be updated via an ActionLink or EventLink component, or by a Form. All of these components support a zone parameter, which is the id of the Zone&apos;s &lt;div&gt;. Clicking such a link will invoke an event handler method on the server as normal ... except that the return value of the event handler method is used to send a _partial page response_ to the client, and the content of that response is used to update the Zone&apos;s &lt;div&gt; in place.

h3. Zone div vs. update div

In many situations, a Zone is a kind of &quot;wrapper&quot; or &quot;container&quot; for dynamic content; one that provides a look and feel ... a bit of wrapping markup to create a border. In that situtation, the Zone &lt;div&gt; may contain an update &lt;div&gt;.

An update &lt;div&gt; is specifcally a &lt;div&gt; element with the CSS class &quot;t-zone-update&quot;, _inside_ the Zone&apos;s &lt;div&gt;.

When an update occurs, the update &lt;div&gt;&apos;s content will be changed, rather than the entire Zone &lt;div&gt;.

The show and update functions apply to the Zone &lt;div&gt;.

h3. Event Handler Return Types

In a traditional request, the return value of an event handler method is used to determine which page will render a _complete_ response, and a _redirect_ is sent to the client to render the new page (as a new request).

With a Zone update, the return value is used to render a _partial response_ within the _same request_.

This return value is typically an injected component or block. The value will be rendered, and that markup will be used on the client side to update the Zone&apos;s &lt;div&gt;.

An event handler may return a [Link|../apidocs/org/apache/tapestry5/Link.html] and the client will be redirected to that link.

Returning a page name (as a String), or a page class, or a page instance will also send a redirect to the indicated page.

h3. Multiple Zone Updates

An event handler may cause multiple zones to be updated on the client side. To accomplish this, return a [MultiZoneUpdate|../apidocs/org/apache/tapestry5/ajax/MultiZoneUpdate.html] object configured with the zones to update. You must know the client-side id for each zone to update (the best way for this is to lock down the zone&apos;s id using the id parameter of the Zone component).

The renderer for each zone can be a block or component, or a [Renderable|../apidocs/org/apache/tapestry5/Renderable.html] or [RenderCommand|../apidocs/org/apache/tapestry5/runtime/RenderCommand.html] ... or an object, such as String, that can be coerced to either of these. Typically, you will inject a Block or Component and return that:

{code|borderStyle=solid}
  @Inject
  private Form registrationForm;

  @Inject Block registrationHelp;

  Object onActionFromRegister()
  {
    return new MultiZoneUpdate(&quot;userInput&quot;, registrationForm).add(&quot;helpPanel&quot;, registrationHelp);
  }

This implies that there are two zones, "userInput" and "helpPanel", somewhere in the rendered page, waiting to receive the updated content.

Graceful Degradation

Users who do not have JavaScript enabled may click ActionLinks that are configured to update a Zone.

When that occurs, the request will still be sent to the server, but will be handled as a traditional request.

To support graceful degradation, you should detect that case and return a traditional response: a page, page name or page class.

This is accomplished by injecting the [Request|../apidocs/org/apache/tapestry5/services/Request.html] object, and invoking the isXHR() method. This value will be true for Ajax requests, and false for traditional request.

Zone Functions

A Zone may be initially visible or invisible. When a Zone is updated, it is made visible if not currently so. This is accomplished via a function on the Tapestry.ElementEffect client-side object. By default, the show() function is used for this purpose. The Zone's show parameter is the name of a Tapestry.ElementEffect function.

If a Zone is already visible, then a different function is used to highlight the change. Here it is the Zone's update parameter, and a default highlight() function, which perfroms a yellow fade to highlight that the content of the Zone has changed.

Zone Limitations

Unlike many other situations, Tapestry relies on you to specify useful and unique ids to Zone components, then reference those ids inside ActionLink components. Using Zone components inside any kind of loop may cause additional problems, as Tapestry will uniqueify the client id you specify (appending an index number).

The show and update function names are converted to lower case; all the methods of Tapestry.ElementEffect should have all lower-case names. Because client-side JavaScript is so fluid (new methods may be added to existing objects), Tapestry makes no attempt to validate the function names ... however, if the names are not valid, then the default show and highlight methods will be used.

Coming Soon

  • Extending a Form with a Zone
  • Additional Tapestry.ElementEffect functions, plus documentation
  • Real examples ...

    Your own Ajax Components

A study of the Autocomplete mixin's code should be very helpful: it shows how to ask the ComponentResources object to create a link.

The key part is the way Tapestry invokes a component event handler method on the component.

For an Ajax request, the return value from an event handler method is processed differently than for a traditional action request. In an normal request, the return value is the normally name of a page (to redirect to), or the Class of a page to redirect to, or an instance of a page to redirect to.

For an Ajax request, a redirect is not sent: any response is rendered as part of the same request and sent back immediately.

The possible return values are:

  • A Block or Component to render as the response. The response will be a JSON hash, with a "content" key whose value is the rendered markup. This is the basis for updates with the Zone component.
  • A [JSONObject|../apidocs/org/apache/tapestry5/json/JSONObject.html] or [JSONArray|../apidocs/org/apache/tapestry5/json/JSONArray.html], which will be sent as the response.
  • A [StreamResponse|../apidocs/org/apache/tapestry5/StreamResponse.html], which will be sent as the response.

    Client-side Logging

Tapestry uses a modified version of the Blackbird JavaScript console. The Tapestry object includes three functions: debug, warn and error.

Each of these functions take a message and an optional pattern; if the pattern is provided, the message is interpolated on the pattern. The final message is displayed in the Blackbird console, which will make itself visible automatically.

In production mode, debug messages will be filtered out (they will not be visible until the user presses F2 to display the console, and then clicks the greyed out icon for debug messages). In development mode, debug messages are not filtered out.

Example usage:

Unknown macro: {code|borderStyle=solid}

Tapestry.debug("Field id is #

Unknown macro: {id}

, value is #

Unknown macro: {value}

", field);

Tapestry.error("Server is not available.");


Handling Slow Page Loads

If your page loads slowly (typically, because of scripts loaded from external sites), you may see a race condition where the user can click on a link before an event handler for that link has been wired up.

The client-side function }}Tapestry.waitForPage(){{ can be used in an element's onclick handler to force a wait for the page to fully load. In this race condition, the screen will dim and a message will appear advising the user to wait a moment; once the page is fully loaded, this modal dialog will be removed.

The correct usage is:

<a href="..." onclick="javascript:Tapestry.waitForPage(event);"> ... </a>


The constant [MarkupConstants.WAIT_FOR_PAGE|../apidocs/org/apache/tapestry5/MarkupConstants.html] contains this snippet.

h1. The Standard Tapestry Library

Tapestry&apos;s client-side support, the standard Tapestry library, is the file tapestry.js, which has dependencies on Prototype and on Scriptaculous Effects. tapestry.js, along with its dependencies, are automatically added to the page when your code adds any other JavaScript or JavaScript library.

h2. Tapestry Namespace

Tapestry defines a number of object and classes inside the Tapestry namespace.

It also adds a handful of methods to the Form class, and to Form elements. These are mostly related to input validation and determining element visibility.

h2. The Tapestry Object

The standard library adds a new function, }}$T(){{. This function is used much like Prototype&apos;s }}$(){{, except that instead of returning a DOM object, it returns a hash (an initially empty JavaScript object) that is associated with the DOM object. This hash is known as _the Tapestry object_.

You may pass in an object id (as a string) or an object reference. The Tapestry Object is created on first invocation. Note: you&apos;ll see it as a property name _tapestry on the DOM object (which may be useful when debugging).

When Tapestry adds information to a DOM object, it does so in the Tapestry object. This helps avoid name conflicts, and groups all Tapestry-added properties into one place which is much easier to debug.

For example, you might store a value for an element in one place:

{code|borderStyle=solid}
  $T(myid).fadeDuration = .5;

Then use it somewhere else:

Unknown macro: {code|borderStyle=solid}

new Effect.Fade($(myId),

Unknown macro: { duration}

);


  • No labels