blob: 3ba7d7cedc17d5a6148be8b203abb34bada00456 [file] [log] [blame]
------
Ajax/DHTML Guide - Basics
------
Jesse Kuhnert
------
24 June 2007
------
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 {{{http://dojotoolkit.org}Dojo}}
javascript toolkit and provides a couple of options for including it for you automatically via the {{{../components/general/shell.html}Shell}} or
{{{../components/general/scriptincludes.html}ScriptIncludes}} components. An example of defining a basic outer layer using the {{{../components/general/shell.html}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 {{{../components/link/directlink.html}DirectLink}}, {{{../components/form/linksubmit.html}LinkSubmit}},
{{{../components/form/form.html}Form}}, {{{../components/form/imagesubmit.html}ImageSubmit}} and {{{../components/form/submit.html}Submit}}. We'll build on our previous
example and use the {{{../components/link/directlink.html}DirectLink}} component to refresh a current time display.
+-------------------------------------------------------------------------------------------------------------
..
<p>Basic javascript inclusion sample.</p>
<p>
<a jwcid="@DirectLink" listener="listener:setTime" 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 {
public abstract void setTime(Date time);
public void setTime()
{
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 {{{../apidocs/org/apache/tapestry/IComponent.html#getClientId()}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 {{{../components/general/for.html}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 {{{../components/form/form.html}Forms}} / {{{../components/general/for.html}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
{{{../apidocs/org/apache/tapestry/AbstractComponent.html#renderIdAttribute(org.apache.tapestry.IMarkupWriter,%20org.apache.tapestry.IRequestCycle)}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>
+----------------------------------------------
[[1]] <<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>
+----------------------------------------------
[[1]] <<form component?>> - If the component in question implements {{{../apidocs/org/apache/tapestry/form/IFormComponent.html}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
{{{../components/form/form.html}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 {{{../components/general/any.html}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 {{{../components/general/any.html}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.