Anda di halaman 1dari 5

bpcatalog: AJAX Auto-Completion Custom JSF Component: Design Details Page 1 of 5

AJAX Auto-Completion Custom JSF Component: Design Details


Tor Norbye
Status: In Early Access
This blueprint demonstrates how to create a custom JSF component which provides auto-completion. The component is generic and can be used to show completion
results for any type of data. The application using the component simply configures the component with a particular method to be called to return completion results.
All the plumbing is provided by the component itself.
To demonstrate its usage, the blue print provides a sample JSF application which uses the component. The application is an address form, containing a number of
JSF text field components, as well as message components that list validation errors such as missing field entries. On submit, the address is stored into a session
scope managed bean, and a confirmation page shows the entered address.
To demonstrate how the component can be configured in different ways, the autocompletion component is used in two places in the form: both for the City field, and
for the State field. The City field provides completion of U.S. cities, and the State field provides completion fo U.S. States.

In the JSP, the two components are simply configured with different completionMethod method bindings. The state completion field looks like this:

<ajaxTags:completionField size="2"
id="stateField" completionMethod="#{ApplicationBean.completeState}"
value="#{SessionBean.state}" required="true" />

Note that the above is the only markup the application developer needs to add to the JSP page. The CSS and JavaScript which implements the
dynamic behavior is generated into the HTML page by the component itself.
The JSF application contains an application-scope level managed bean named ApplicationBean, which has a method named completeState. It is referenced
in the method binding expression above. Whenever the user focuses or types in the State text field, the above method will be called asynchronously, and its return
values displayed in a popup in the browser.
Implementing the completion popup method is simple:

private String[] states =


new String[] {
"AK", "AL", "AR", "AZ", "CA", "CO", "CT", "DC", "DE", "FL",
... };
public void completeState(FacesContext context, String prefix, CompletionResult result) {
AjaxUtilities.addMatchingItems(states, prefix, result);
}

AjaxUtilities.addMatchingItems is a simple utility method provided with the component provides a simple binary search in a sorted array of
items and finds the best 10 matching entries. These are then added to the result by calling CompletionResult.addItem() for each matching
string to be displayed.
That's all there is to it from the application developer's perspective - simply include the component, bind it to a method which returns completion results, and you're
done.
The above JSP tag will cause the following HTML to be rendered in the browser:

https://bpcatalog.dev.java.net/ajax/textfield-jsf/design.html 1/25/2008
bpcatalog: AJAX Auto-Completion Custom JSF Component: Design Details Page 2 of 5

<div id="menu-popup1" style="position: absolute; top:170px;left:140px;visibility:hidden">


<table bgcolor="#fffafa" border="1" bordercolor="black" cellspacing="0" cellpadding="0"></table>
</div>
<input
id="autofillform:stateField" autocomplete="off" type="text"
name="autofillform:stateField" size="2"
onfocus="doCompletion('autofillform:stateField','menu-popup1','#{ApplicationBean.completeState}',null,null);"
onkeyup="doCompletion('autofillform:stateField','menu-popup1','#{ApplicationBean.completeState}',null,null);"
onblur="stopCompletionDelayed('menu-popup1');"
/>

The first div above is the popup menu. It is still hidden (as can be seen from the CSS visibility:hidden property, but when the JavaScript code receives a set
of items from the server, this tag will be made visible and new completion items will dynamically be added as children.
The challenging portion is implementing such as JSF component, and that is the focus of this blueprint.

Architecture Overview
The architecture of the component is shown in the following illustration:

As seen in the architecture diagram, the solution relies on both a client side portion and a server side portion. On the server side, there's a JSF textfield component
which renders HTML for the browser, and embeds JavaScript callbacks into the HTML. When this JavaScript is executed in the browser, it results in a call to the
server. This call is intercepted by the PhaseListener. The PhaseListener will call application methods to generate the completion items, and return these to the
browser JavaScript.

Hooking Into JSF: The PhaseListener


The JSF servlet is configured with an additional PhaseListener (a JSF application can be configured by many PhaseListeners, so an application can include many
different component libraries that each rely on their own PhaseListeners).
This is done by adding the following code to the faces-config.xml JSF configuration file:

<!-- Handle AJAX component requests - completion requests, script requests


and stylesheet requests -->
<lifecycle>
<phase-listener>com.sun.j2ee.blueprints.bpcatalog.ajax.components.AjaxPhaseListener</phase-listener>
</lifecycle>

The PhaseListener will be called when new requests arrive. It looks at the URL, and decides if this is an AJAX related request. If so, it "intercepts" the requests, by
writing the response, and calling FacesContext.responseComplete(true) which will cause the JSF machinery to ignore the requests. In other words, it
carves out a namespace for special AJAX requests, and leaves the rest to normal JSF handling.
Specifically, the PhaseListener handles three types of requests:
1. ajax-textfield-script.js: When the URL ends with this, it will write out the JavaScript code to be included in the page. The JSF component renderer
will emit a script-inclusion link which requests this resource. By emitting the JavaScript as a separate file it allows the browser to cache its contents over
multiple pages using the same component, or successive requests for the same page. This URL is requested by the browser when it encounters the <script>
tag in the HTML page. This tag was placed in the HTML in the first place by the text field renderer, discussed later.
2. ajax-textfield-css.css: This will cause the PhaseListener to write out the CSS file containing style rules for the JSF component HTML. This URL is
requested by the browser when it encounters the <link> tag in the HTML page, generated by the component.
3. ajax-autocomplete: This URL is a requests for completion items. This URL is requested by JavaScript code in the browser when it receives a key event.

https://bpcatalog.dev.java.net/ajax/textfield-jsf/design.html 1/25/2008
bpcatalog: AJAX Auto-Completion Custom JSF Component: Design Details Page 3 of 5

The response is written as an XML document containing a list of items. The URL contains parameters and is processed by the PhaseListener like a normal
servlet. For example, it will fetch the prefix to be matched from the master list of items by looking up the HttpServletRequest from the PhaseEvent, and
then get the prefix like this:

String prefix = request.getParameter("prefix");


String completionMethod = request.getParameter("method");

The completion method is also looked up, and an actual method binding is evaluated by the following code:

Class[] argTypes = { FacesContext.class, String.class, CompletionResult.class };


MethodBinding vb = context.getApplication().createMethodBinding(completionMethod, argTypes);
Object[] args = { context, prefix, result };
vb.invoke(context, args);

The Component
The AjaxTextField component is a lot like the standard HtmlInputText component. It's a fairly simple class which simply provides a number of properties -
such as the value (the text in the text field), the maximum length of allowable input, the numer of visible columns, and so on. All the hard work is done in the Renderer
for the text field, which generates HTML for the browser to visualize the component.
The component differs from the standard text field in that it contains an additional property: completionMethod. This is simply a String, which should be a method
binding for a method which takes three parameters: a FacesContext, a String prefix parameter, and a result method that the user of the component calls
addItem on to add items to be shown in the completion popup.

The Component Renderer


The component renderer is fairly simple: it generates an HTML <input> tag of type text. In addition, it emits a special nonstandard HTML attribute for HTML text
fields to turn off native browser completion. Modern browsers typically offer their own auto-completion capabilities for textfields - usually suggesting previously entered
data for this text field - and we need to turn that off or otherwise the user will see two conflicting popups. The way to do this is to emit

<input type="text" autocomplete="off" value="Hello World"/>

Second, the renderer needs to ensure that the custom JavaScript and CSS files are included in the page. It does this by rendering <script> and <link> tags with a
special URL. The URL is recognized by the PhaseListener and will return the JavaScript and CSS files stored with the component. This allows the supporting files to
live with the bundled component, rather than in the application's own directories. As a result, page developers simply need to include the component jar with
deployment, rather than needing to include supporting references next to the pages.
Note however that there could be multiple instances of the autocompletion text field on the page. Indeed, in the blueprint example, it is used both as a City field and as
a State field. This however means that we need to ensure that the JavaScript is parameterized (so it can be shared by the components), and written out exactly once.
(The alternative is to write out the JavaScript for each component and ensure that all the function names are unique, but this has obvious disadvantages so is not
recommended.)
A simple way to ensure that the JavaScript and CSS links are emitted exactly once per page is to use the RequestMap. When we render the script and CSS links, we
put a flag into the RequestMap. Before rendering these tags, we check the request map for the flag to see if this page already has rendered the component. (Note that
you cannot store a flag on the renderer since one is shared for multiple pages and requests.) Here's the code to achieve that:

...
private void renderScriptOnce(ResponseWriter writer, UIComponent component, FacesContext context)
throws IOException {
// Store attribute in request map when we've rendered the script such
// that we only do this once per page
Map requestMap = context.getExternalContext().getRequestMap();
Boolean scriptRendered = (Boolean)requestMap.get(RENDERED_SCRIPT_KEY);

if (scriptRendered == Boolean.TRUE) {
return;
}
// Render script and CSS here
...
}
private static final String RENDERED_SCRIPT_KEY = "bpcatalog-ajax-script-rendered";

Finally, the Renderer emits JavaScript hooks for the input text field: onkeyup, onfocus, and onblur JavaScript fragments which calls functions in the generic
JavaScript with parameters referencing this specific text field and its completion method. The net result is that the HTML page ends up containing a JavaScript file
include, and a number of HTML textfields with JavaScript handlers which calls methods to initiate asynchronous calls when keys are pressed.

The Clientside JavaScript


The JavaScript code needs to do three things:
1. Respond to focus and keypress events by fetching the current textfield value, and sending it to the server for evaluation.
2. Respond to asynchronous responses from the server with completion results, and update the HTML document to show these items. This either means opening
a popup window, or updating the items in an existing popup window.
3. Respond to user selection of an item in the popup: populate the associated textfield with the selected item and close the popup.
To request a server validation, each keypress generates an XMLHttpRequest object. This requires some browser specific code:

function initRequest(url) {
if (window.XMLHttpRequest) {

https://bpcatalog.dev.java.net/ajax/textfield-jsf/design.html 1/25/2008
bpcatalog: AJAX Auto-Completion Custom JSF Component: Design Details Page 4 of 5

req = new XMLHttpRequest();


} else if (window.ActiveXObject) {
isIE = true;
req = new ActiveXObject("Microsoft.XMLHTTP");
}
}

This request object is then populated with a URL which encodes the parameters we want to pass to the PhaseListener: the completion method binding
expressions, and the prefix to be matched (e.g. the text entered in the text field so far):

...
var url = "faces/ajax-autocomplete?action=complete&method=" + escape(method) + "&id=" + escape(target.value);
initRequest(url);
req.table = table;
req.targetName = targetName;
req.onreadystatechange = processRequest;
req.open("GET", url, true);
req.send(null);

The onreadystatechange property specifies a callback method to be invoked when the response arrives later, and the asynchronous request is
then submitted.
The XMLHttpRequest object will invoke the callback potentially many times, with different codes.

function processRequest(table) {
if (req.readyState == 4) { /* Request processing complete */
if (req.status == 200) { /* http: success */
var table = req.table;
parseMessages(table);
}
}
}

When the readyState is 4 and the status is 200, we have a successful XML document response from the server we need to parse and handle. Fortunately, that's very
simple to do from JavaScript, since we have access to DOM APIs. For example, since we know the XML responses will be of this type:

<items>
<item>Hello</item>
<item>World</item>
</items>

we can write JavaScript like this to iterate over the items:

var items = req.responseXML.getElementsByTagName("items")[0];


for (loop = 0; loop < items.childNodes.length; loop++) {
var item = items.childNodes[loop];
appendItem(names, item.childNodes[0].nodeValue);
}

Here, appendItem is another JavaScript function which dynamically pops up the menu if necessary, and adds the given String as an item into
the popup's list of items.
The final trick is being able to show a popup from JavaScript, located right below the text field. This involves using CSS positioning, as well as some browser-specific
tricks. The details of this can be found in the code. Clearly this can be made more sophisticated by adding keyboard handling (so arrow up, down and enter can
navigate through items) and so on.

Custom Handlers
The Address Form example brings up an interesting possibility. Since the server not only knows all the U.S. cities, but it also knows the associated states and zip
codes, why not let the user simply select a city from the popup, and based on the server information populate all three textfields automatically - city, state and zip.

Screenshot of deployed sample application with completion on the city field

https://bpcatalog.dev.java.net/ajax/textfield-jsf/design.html 1/25/2008
bpcatalog: AJAX Auto-Completion Custom JSF Component: Design Details Page 5 of 5

This can be accomodated fairly easily. The Ajax Text Field exposes two custom JavaScript hooks: ondisplay and onchoose.

We want the server response to include all the information (city, state and zip), yet the popup should not contain the zip codes. Thus, the first hook, ondisplay, lets
the application developer hook in custom JavaScript to massage the display items arriving from the server before they are placed into the popup.
Thus, in the address form application, the completeCity method on the server will return responses of the form

<item>Hollywood, CA 90210</item>

In the JSP the user of the JSF component has added the following custom JavaScript function, which is passed in the server item String to be
displayed and returns the String to be shown in the popup:

function extractCity(citystatezip) {
var index = citystatezip.indexOf(',');
var nextcity = citystatezip.substring(0, index+4);
return nextcity;
}

This simply strips out the zip code and returns the "city, state" pair.
Similarly, when the user selects a city, we want to populate all three of the city, state and zip fields. We can provide another JavaScript method which does this: it is
passed in the server String to that was chosen, and it can process this to populate other text fields or further massage the values:

function chooseCity(city) {
var index = city.indexOf(',');
var state = city.substring(index+2, index+4);
var zip = city.substring(index+5);
city = city.substring(0, index);
document.getElementById('autofillform:cityField').value = city;
document.getElementById('autofillform:stateField').value = state;
document.getElementById('autofillform:zipField').value = zip;
}

Finally, these custom handlers are hooked up to the component's ondisplay and onchoose properties in the JSP as follows:

<ajaxTags:completionField size="40" id="cityField"


completionMethod="#{ApplicationBean.completeCity}"
value="#{SessionBean.city}" required="true"
ondisplay="function(item) { return extractCity(item); }"
onchoose="function(item) { return chooseCity(item); }"
/>

This is certainly expert usage, but it allows some really powerful usages of the autocompletion component, with all the asynchronous calls and plumbing handled by
the component. The normal mode of operation is the completion used by the state field, which simply picks from a simple array, and all the component developer has
to do is hook up a single completion method which returns a set of items, and everything else is handled automatically.

© Sun Microsystems 2005. All of the material in The Java BluePrints Solutions Catalog is copyright-protected and may not be published in other works without express written
permission from Sun Microsystems.

https://bpcatalog.dev.java.net/ajax/textfield-jsf/design.html 1/25/2008

Anda mungkin juga menyukai