Building Ajax-enabled auto-complete and cascading drop-down controls

Using JSP TagLib, JSON, and Ajax

Two key technologies enable next-generation Web sites: Ajax and JSON. Business-line applications can benefit from these technologies to provide more intuitive and responsive user interfaces. This article describes how to add Ajax and JSON to Java™ Platform, Enterprise Edition (Java EE) Web applications by building reusable JSP TagLib controls based on Ajax.

Within the article, I show how to build a cascading drop-down control that dynamically populates values in an HTML SELECT control based on other form field values. I also describe how to build an auto-complete control, similar to Google Suggest, that displays a suggestion list that is updated in real time as a user types. You'll build the controls by integrating JSON, JavaScript, CSS, HTML, and Java EE technologies.

Technical overview

The primary design goals of the controls developed in this article are:

Provide easy integration with existing Web applications. The controls should encapsulate all logic and JavaScript code to simplify the deployment process.

Be configurable.

Minimize data- and page-size overhead.

Leverage CSS and HTML standards.

Provide cross-browser support (Microsoft® Internet Explorer, Mozilla Firefox).

Leverage common design patterns/best practices to improve code maintainability.

To meet the goals of easy integration and configurable controls, the article's examples use configurable tag attributes where possible. In addition, you define interface/contracts to provide a straightforward way to integrate custom data/value providers with the controls.

The article uses an additional control to encapsulate common JavaScript functions, and thereby minimizing data and overhead. You use JSON to minimize data exchange when making asynchronous calls.

The articles's examples use Web standards, including CSS and HTML, for cross-browser support. The JavaScript, HTML, and CSS emitted by the controls are tested against Internet Explorer 7.x and Mozilla Firefox 2.x/3.x.

The data and value providers are built based on common Object-Oriented Programming design patterns and best practices, such as an n-tier architecture, the adapter design pattern, and interface-based programming.

Technical considerations for implementing the example controls

There are a few key technical considerations for the Ajax-enabled controls you develop in this article, including the mechanism to provide values to the Ajax controls, a data-exchange format for asynchronous communication, the class design, and the data model.

Mechanism to provide responses to asynchronous calls

You have three options to expose data asynchronously to Ajax-enabled controls:

JavaServer Pages (JSP)

Servlets

SOAP or RESTful Web services

This article uses Servlets due to their efficiency and minimal overhead. A JSP page is simpler to implement than a Servlet, but it isn't as clean from an implementation perspective.

Data-exchange format considerations

Data providers for Ajax-enabled controls can use XML or JSON as the data-exchange format. XML is generally more human-readable than JSON, but it has the following disadvantages:

Larger data size compared to JSON

Slightly more difficult to parse within JavaScript

For these reasons, this article uses JSON.

Data model

The data model for the sample application comprises two entities:

State, which contains state abbreviations and names

Location, which contains city, zip code, and other location data

Figure 1 shows the data model used for the sample pages in this article.

Figure 1. Data Model

Class model

The example in this article consists of the Data Abstract Layer (DAL), Data Transfer Objects (DTOs), the Business Logic Layer (BLL), the Presentation Layer, and supporting helper classes. The following figures show UML class diagrams for these classes.

The helper classes provide database and presentation layer supporting classes (see Figure 2).

Figure 2. UML class diagram -- helper classes

The Data Abstract Layer consists of a single class to provide location-related data to the business layer (see Figure 3).

Figure 3. UML class diagram -- Data Abstract Layer classes

You use two DTOs to pass data through the three tiers (see Figure 4). StateDTO holds state-related data, and LocationDTO holds location-related data, including zip code, city name, state, latitude, and longitude.

Figure 4. UML class diagram -- Data Transfer Object classes

The Business Logic Layer consists of the value providers that provide data to the Ajax-enabled controls (see Figure 5). Value providers for the auto-complete control must implement the IJsonValueProvider interface. The location service receives the collection of DTO objects from the data layer and generates the corresponding JSON data for use in the presentation layer.

Figure 5. UML class diagram -- Business Logic Layer classes

The Servlets provide the interface to which the client-side asynchronous calls are made (see Figure 6). These Servlets interact with the value providers to provide JSON data to the Web browser.

Figure 6. UML class diagram -- Business Logic Layer, Servlet classes

JSP TagLib controls

You'll create the following Ajax-enabled controls:

Cascading drop-down control -- Dynamically populates value options in SELECT controls based on other form fields or business rules.

-- Dynamically populates value options in controls based on other form fields or business rules. Auto-complete control -- Displays a suggestion list, similar to Google Suggest, in real time as a user types. The suggestions are dynamically displayed using asynchronous communication with a data provider servlet.

In addition to the two JSP TagLib controls, you need a third control to encapsulate all reusable JavaScript functions, such as clearing/populating values, handling keyboard/mouse events, and supporting asynchronous communication. Figure 7 illustrates these three control classes.

Figure 7. UML class diagram -- JSP TagLib control classes

Build the data provider and data layer

The LocationDataService class is a data provider to retrieve location-related data from the database. It returns TreeMap objects containing LocationDTO and StateDTO objects. It's highly recommended that the data provider cache the results in memory for optimal performance, particularly because the data is consumed through asynchronous server calls.

Build a JSP TagLib control

You create a JSP TagLib control by extending TagSupport or TagBodySupport and overriding the doStartTag() , doAfterBody() , or doEndBody() method to render the control's content (HTML code, JavaScript) during page processing. Listing 1 shows an example of an overridden doStartTag method.

Listing 1. JdbcQuery class

/* (non-Javadoc) * @see javax.servlet.jsp.tagext.TagSupport#doStartTag() */ @Override public int doStartTag() throws JspException { JspWriter out = pageContext.getOut(); try { // An example of rendering output within a JSP page out.print("This is a string that will be rendered"); // A more practical example out.print("<h1 id='heading1'>This is a Heading</h1>"); } catch (IOException e) { e.printStackTrace(); }

After you create the implementation of the JSP TagLib control, you must define a TagLib Library Definition (TLD) in the /WEB-INF/tlds directory, as shown in Listing 2.

Listing 2. Sample JSP TagLib library definition file

<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd"> <taglib> <tlibversion>1.0</tlibversion> <jspversion>1.1</jspversion> <shortname>ajax</shortname> <info>Ajax control library</info> <tag> <name>sample</name> <tagclass>com.testwebsite.controls.SampleJspTag</tagclass> <bodycontent>JSP</bodycontent> <info> This is a sample control </info> <attribute> <name>id</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag> </tagLib>

You can place the control in any JSP page by adding the code from Listing 3.

Listing 3. Sample JSP page

<%@ page contentType="text/html; charset=ISO-8859-5" %> <%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%> <html> <head> <title>This is a test page</title> <link href="core.css" rel="stylesheet" type="text/css" /> </head> <body> This is a test page. <ajax:sample/> </body> </html>

Build the Ajax page JSP TagLib control

The <ajax:page/> control renders the standard JavaScript functions that are needed to add asynchronous support to JSP pages. It also renders helper functions for the <ajax:autocomplete/> and <ajax:dropdown/> controls. The helper functions are covered in the respective control sections Build the auto-complete JSP TagLib control and Build the cascading drop-down JSP TagLib control. Where possible, it's best to keep supporting JavaScript functions in the <ajax:page/> control rather than in individual controls because doing so reduces the page size. Alternatively, you can store them in an external JS file, but that slightly complicates deployment by reducing encapsulation within the control.

The XMLHttpRequest object, which is accessible in JavaScript, is central to asynchronous Web communication. Unfortunately, XMLHttpRequest isn't an approved standard, and vendor support varies slightly. For Opera, Mozilla Firefox, and Microsoft Internet Explorer 7.0 and later, it's a matter of using the new XMLHttpRequest() JavaScript syntax. For prior versions of Microsoft Internet Explorer, you create the object using new ActiveXObject('Microsoft.XMLHTTP') . Listing 4 shows how to initialize the XMLHttpRequest for cross-browser support.

Listing 4. Create the XMLHttpRequest object

var req; function initializeXmlHttpRequest() { if (window.ActiveXObject) { req=new ActiveXObject('Microsoft.XMLHTTP'); } else { req=new XMLHttpRequest(); } }

As mentioned, you render JavaScript code to a page by adding the code from Listing 5 to the tag-implementation class.

Listing 5. Render the JavaScript initialization function for the XMLHttpRequest object

/* (non-Javadoc) * @see javax.servlet.jsp.tagext.TagSupport#doStartTag() */ @Override public int doStartTag() throws JspException { StringBuffer html = new StringBuffer(); html.append("<script type='text/javascript' language='javascript'>"); html.append("var req;"); html.append("var cursor = -1;"); // Generate functions to support Ajax html.append("function initializeXmlHttpRequest() {"); // Support for non-Microsoft browsers (and IE7+) html.append("if (window.ActiveXObject) {"); // Support for Microsoft browsers html.append("req=new ActiveXObject('Microsoft.XMLHTTP');"); html.append("}"); html.append("else {"); html.append("req=new XMLHttpRequest();"); html.append("}"); JspWriter out = pageContext.getOut(); try { out.append(html.toString()); } catch (IOException e) { e.printStackTrace(); } return this.SKIP_BODY; }

The req variable can now be used globally within the Web page. Listing 6 illustrates how to make an asynchronous call.

Listing 6. Use the XMLHttpRequest object

// If req object initialized if (req!=null) { // Set callback function req.onreadystatechange=stateName_onServerResponse; // Set status text in browser window window.status='Retrieving State data from server...'; // Open asynchronous server call req.open('GET',dataUrl,true); // Send request req.send(null); }

When the ready state of a request changes, the function specified in req.onreadystatechange is invoked. req.readystate contains one of the following status codes:

0=initialized

1=Open

2=Sent

3=Receiving

4=Loaded

Use XML as a data-exchange format The XMLHttpRequest object also has a responseXML property to retrieve XML data responses. JavaScript's DOM can then be used for processing.

Normally, anything but Loaded is ignored because typically nothing needs to occur until the server response is complete. A value of Loaded for an asynchronous call doesn't guarantee success. As with any Web page request, it's possible that the page wasn't found or that another problem occurred. If the req.status is anything but 200 , something went wrong. Listing 7 shows how to handle the server response.

Listing 7. Handle the response to the asynchronous request

function stateName_onServerResponse() { if(req.readyState!=4) return; if(req.status != 200) { alert('An error occurred retrieving data.'); return; } // Obtain server response var responseData = req.responseText; ... Processing of result }

You've now had a good overview of how to make asynchronous calls and handle responses. The next step is to start building the first control: <ajax:autocomplete/> .

Build the auto-complete JSP TagLib control

The following steps are required to build the auto-complete control:

Build a value provider to supply suggestions for the control. Create a Servlet interface to expose the value provider for asynchronous calls. Create a JSP TagLib control to encapsulate everything in a control that can be used in JSP pages.

The following sections explain these steps in detail.

Build the value provider to supply suggestions for the auto-complete control

The value provider supplies the suggestion list to the auto-complete control. A value provider must implement the IJsonValueProvider interface, which defines a single method getValues() that returns a JSONArray object containing the suggestion list. The interface is shown in Listing 8.

Listing 8. IJsonValueProvider interface

public interface IJsonValueProvider { JSONArray getValues(String criteria, Integer maxCount); }

JSONObject and JSONArray These objects are part of JSON for Java , an open source wrapper for using JSON in Java. Refer to Related topics for additional information regarding this library.

The next step is to create CityValueProvider , the implementation of this interface, which provides city data for the <ajax:autocomplete/> control. Note the following key things about the getValues() implementation:

Data is retrieved from the Location Data Provider -- a Data Abstract Layer (DAL) component -- which caches all locations in memory.

A two-phase approach is required to process the data ( TreeMap containing LocationDTO objects), because the Location Data Provider returns a TreeMap sorted by zip code. The results need to be sorted based on the city name for the CityValueProvider .

Listing 9 illustrates how this is done.

Listing 9. City value provider

package com.testwebsite.bll; import java.util.Iterator; import java.util.Set; import java.util.TreeMap; import org.json.JSONArray; import com.testwebsite.dal.LocationDataService; import com.testwebsite.dto.LocationDTO; import com.testwebsite.interfaces.IJsonValueProvider; /** @model * @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com) */ public class CityValueProvider implements IJsonValueProvider { /* (non-Javadoc) * @see com.testwebsite.interfaces.IJsonValueProvider#getValues(java.lang.String) */ @Override public JSONArray getValues(String criteria, Integer maxCount) { String cityName = ""; // If city found, make the search case insensitive if (criteria != null && criteria.length() > 0) { cityName = criteria.toLowerCase(); } // Get Location data from Data Provider TreeMap<Integer, LocationDTO> locData = LocationDataService.getLocationData(); // The LocationDataService Data Provider returns a TreeMap containing // LocationDTO objects that are sorted by Zip Code. // First build a temporary TreeMap (sorted list) filtering with // only unique city names matching the specified cityName parameter TreeMap<String, String> cityData = this.getCityData(locData, cityName); // Finally iterate through sorted City list // and create JSONArray containing // the number elements specified by the maxCount parameter JSONArray json = this.getJsonData(cityData, maxCount); return json; } /** * The getCityData method returns a TreeMap containing Cities matching the * specified cityName criteria. The results are sorted by City Name and filter * out any duplicate city names. * @param locData Location Data from which to retrieve cities * @param cityName City Name prefix to which to search * @return */ protected TreeMap<String, String> getCityData( TreeMap<Integer, LocationDTO> locData, String cityName) { TreeMap<String, String> cityData = new TreeMap<String, String>(); // Iterate through all data looking for matching cities // and add to temporary TreeMap Set<Integer> keySet = locData.keySet(); Iterator<Integer> locIter = keySet.iterator(); while (locIter.hasNext()) { // Get current state Integer curKey = locIter.next(); LocationDTO curLocation = locData.get(curKey); // Get current location data if (curLocation != null) { String curCityName = curLocation.getCity().toLowerCase(); // Add current item if it starts with the cityName parameter if (curCityName.startsWith(cityName)) { cityData.put(curLocation.getCity(), curLocation.getCity()); } } } return cityData; } /** * The getJsonData method returns a JSONArray contain a list of strings * with the city name specified with a maximum number of elements as specified * by the maxCount parameter. * @param cityData TreeMap containing unique list of matching cities * @param maxCount Maximum number of items to include in the JSONArray * @return JSONArray contain sorted list of city names */ protected JSONArray getJsonData(TreeMap<String, String> cityData, int maxCount) { int count = 1; JSONArray json = new JSONArray(); // Get city name keys Set<String> citySet = cityData.keySet(); // Iterate through query results Iterator<String> cityIter = citySet.iterator(); while (cityIter.hasNext()) { // Get current item String curCity = cityIter.next(); // Add item to JSONArray json.put(curCity); // Increment counter count ++; // If maximum number of entries has been met, then exit loop if (count >= maxCount) break; } return json; } }

Create a Servlet to handle asynchronous requests to the value provider

The next step is to build the AutoCompleteServlet Servlet, the interface for the Web browser to call the IJsonValueProvider implementations. The Servlet is straightforward, with one minor exception. To meet the goal of "easy integration/deployment," you should only need to worry about implementing a value provider, rather than the Servlet interface. To support this goal, you use reflection to instantiate the value provider at runtime using the classname attribute of the <ajax:autocomplete/> control. See Listing 10.

Listing 10. Auto-complete Servlet

package com.testwebsite.servlets; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.json.JSONArray; /** * @model * @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com) */ public class AutoCompleteServlet extends HttpServlet { /** * */ private static final long serialVersionUID = -867804519793713551L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String data = ""; // Get parameters from query string String format = req.getParameter("format"); String criteria = req.getParameter("criteria"); String maxCountStr= req.getParameter("maxCount"); String className = req.getParameter("providerClass"); // If format is not null and it's 'json' if (format != null && format.equalsIgnoreCase("json")) { if (className != null && className.length() > 0) { data = this.getJsonResultAsString(criteria, maxCountStr, className); } resp.setContentType("text/plain"); } // Write response // Get writer for servlet response PrintWriter writer = resp.getWriter(); writer.println(data); writer.flush(); } public String getJsonResultAsString(String criteria, String maxCountStr, String className) { String data = ""; Integer maxCount = 10; if (maxCountStr != null && maxCountStr.length() > 0) { maxCount = new Integer(maxCountStr); } // Get dataprovider class using reflection // Construct class Class providerClass; try { // Get provider class providerClass = Class.forName(className); // Construct method and method param types Class[] paramTypes = new Class[2]; paramTypes[0] = String.class; paramTypes[1] = Integer.class; Method getValuesMethod = providerClass.getMethod("getValues", paramTypes); // Construct method param values Object[] argList = new Object[2]; argList[0] = criteria; argList[1] = maxCount; // Get instance of the provider class Object providerInstance = providerClass.newInstance(); // Invoke method using reflection JSONArray resultsArray = (JSONArray) getValuesMethod.invoke(providerInstance, argList); // Convert JSONArray result to string data = resultsArray.toString(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return data; } }

Figure 8 shows the server response from AutoCompleteServlet Servlet.

Figure 8. Auto-complete Servlet Response

Create a JSP TagLib control that can be used in a JSP page

The auto-complete control renders a standard INPUT tag, sets up the event handlers, and renders the Suggestion List Container DIV element and appropriate CSS for formatting. You need to add the following supporting JavaScript functions:

Handle keyboard events -- <ajax:page/>

Handle server response and post-asynchronous invocation processing -- <ajax:autocomplete/> , with helper functions rendered in <ajax:page/>

, with helper functions rendered in Highlight a specified item in the suggestion list -- <ajax:page/>

Hide the suggestion list -- <ajax:page/>

Handle the selection of an item in the suggestion list (when the user presses Enter) -- <ajax:page/>

Handle when the control loses focus -- <ajax:page/>

Let's start with the onSuggestionKeyDown function, where the Esc key, Enter key, and other control keys are handled. If the user presses Esc, the suggestion list should be hidden and subsequent events in the JavaScript event chain canceled (for example, the Key Up event shouldn't be processed because the event was already handled by hiding the suggestion list); see Listing 11.

Listing 11. Code fragment for handling Esc key

var keyCode = (window.event) ? window.event.keyCode : ev.keyCode; switch(keyCode) { ... // Handle ESCAPE key case 27: hideSelectionList(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; ...

If the user presses Enter, the current item should be copied to the input control and the suggestion list hidden. To hide/display the suggestion list, you use standard CSS for formatting in conjunction with JavaScript to change the class name. The display property is set to none to hide the control and to block to display the list. Listing 12 shows the JavaScript function, which you add to the <ajax:page/> control because it can be used with any <ajax:autocomplete/> control.

Listing 12. Code fragment for handling the Enter key

... // Handle ENTER key case 13: handleSelectSuggestItem(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; ...

The key-down event handler calls handleSelectSuggestItem , which is defined in <ajax:page/> (see Listing 13).

Listing 13. Handling the Enter key

function handleSelectSuggestItem(curControl, suggestionList) { // Get selected node // Cursor is a global variable that is incremented/decremented // when the UP ARROW or DOWN ARROW key is pressed. var selectedNode = suggestionList.childNodes[cursor]; // Get selected value var selectedValue = selectedNode.childNodes[0].nodeValue; // Set the value of the INPUT control curControl.value = selectedValue; // Finally hide the selection list hideSelectionList(curControl, suggestionList); } function hideSelectionList(curControl, suggestionList) { // If suggestion not found if (suggestionList == null || suggestionList == undefined) { return; } // Clear the suggestion list elements suggestionList.innerHTML=''; // Toggle display to none suggestionList.style.display='none'; curControl.focus(); }

There isn't much to handling the pressing of control keys (Shift, Alt, and Ctrl). You need to ignore these keyboard events by doing the following:

Preventing changes to the input control during the return by setting returnValue on the EVENT object to false (for Internet Explorer) and executing preventDefault() on the EVENT object (for Firefox)

on the object to (for Internet Explorer) and executing on the object (for Firefox) Canceling the event chain for the keyboard event by setting the cancelBubble property on the EVENT object to true

The complete code for onSuggestionKeyDown appears in Listing 14.

Listing 14. Complete key-down event handler

function onSuggestionKeyDown(curControl, ev) { // Get suggestion list container var suggestionList= document.getElementById(curControl.id + '_suggest'); // Get key code of key pressed var keyCode = (window.event) ? window.event.keyCode : ev.keyCode; switch(keyCode) { // Ignore certain keys case 16, 17, 18, 20: ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; // Handle ESCAPE key case 27: hideSelectionList(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; // Handle ENTER key case 13: handleSelectSuggestItem(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; } }

The key-up event handler is slightly more interesting. If the user presses the Up Arrow or Down Arrow key, the highlighted selection should change. If the user has entered the minimum number of characters (default: 3), then the asynchronous call should be made to the server to populate the suggestion list.

If the user presses the Up Arrow or Down Arrow key, the global cursor variable is incremented/decremented accordingly. The cursor variable tracks the currently selected item. The highlightSelectedNode function is than called to highlight the value. See Listing 15.

Listing 15. Code fragment of the key-up event handler for handling the Up Arrow and Down Arrow keys

... switch(keyCode) { // Ignore ESCAPE case 27: // Handle UP ARROW case 38: if (suggestionList.childNodes.length > 0 && cursor > 0){ var selectedNode = suggestionList.childNodes[--cursor]; highlightSelectedNode(suggestionList, selectedNode); } break; // Handle DOWN ARROW case 40: if (suggestionList.childNodes.length > 0 && cursor < suggestionList.childNodes.length-1) { var selectedNode = suggestionList.childNodes[++cursor]; highlightSelectedNode(suggestionList, selectedNode); } break; ...

Listing 16 shows the highlightSelectedNode function for highlighting an item. CSS rules are defined for selected and unselected items. The className is toggled using JavaScript. The highlight for the previously selected element is then removed.

Listing 16. Highlight an item in the suggestion list

function highlightSelectedNode(suggestionList, selectedNode) { if (suggestionList == null || selectedNode == null) { return; } // Iterate through all items searching for a node that // matches the node selected for (var i=0; i < suggestionList.childNodes.length; i++) { var curNode = suggestionList.childNodes[i]; if (curNode == selectedNode){ curNode.className = 'autoCompleteItemSelected' } else { curNode.className = 'autoCompleteItem'; } } }

If the user presses any other key and has entered at least the minimum number of characters, an asynchronous call to the server is made to retrieve a JSON array of suggestions. After the ready state changes, the function specified in the req.onreadystatechange property is invoked (see Listing 17).

Listing 17. Code fragment for handling any other key

// If control not found (shouldn't happen) // or minimum number of characters not entered if (curControl == null || curControl.value.length < minChars) { // Hide selected item hideSelectionList(curControl, suggestionList); return; } // Initialize XMLHttpRequest object initializeXmlHttpRequest(); // If req object initialized if (req!=null) { // Set callback function req.onreadystatechange=cityName_onServerResponse; // Set status text in browser window window.status='Retrieving State data from server...'; // Open asynchronous server call req.open('GET',dataUrl,true); // Send request req.send(null); }

When the server-response function is invoked, the readyState is checked to make sure it's Loaded . The status is also checked. If all is well, the string representation of the JSON array is converted to an array using the eval JavaScript function. The array is then passed to the populateSuggestionList function, which adds the elements to the Suggestion List. Listing 18 shows the server-response function.

Listing 18. Server-response handler (generated dynamically by the <ajax:autocomplete/> control)

function cityName_onServerResponse() { // If loaded if(req.readyState!=4) { return; } // If an error occurred if(req.status != 200) { alert('An error occurred retrieving data.'); return; } // Get response and convert it to an array var responseData = req.responseText; var dataValues=eval('(' + responseData + ')'); // Get current control var curControl = document.getElementById('cityName'); /// Populate suggestion list for control populateSuggestionList(curControl, dataValues); }

The populateSuggestionList function, which is rendered in the <ajax:page/> control, is responsible for populating the suggestion list with the values returned from the asynchronous server call. The array is then iterated through, and a DIV element is created for each item in the array. The DIV element is added to the Suggestion List. Listing 19 shows populateSuggestionList .

Listing 19. Populate the suggestion list (rendered in the <ajax:page/> control)

populateSuggestionList(curControl, dataValues) { // Get Suggest List Container for control var container = document.getElementById(curControl.id + '_suggest'); // If container not found (shouldn't happen), then simply return if (container == null) { return; } // Clear suggestion list container container.innerHTML = ''; // If no values return, hide suggestion list if (dataValues.length < 1) { container.style.display='none'; return; } // Show suggestion list container.style.display='block'; container.style.top = (curControl.offsetTop+curControl.offsetHeight) + 'px'; container.style.left = curControl.offsetLeft + 'px'; // Iterate through all values // 1. Create DIV element // 2. Set attributes and text node value // 3. Append new element to the container for(var i=0;i < dataValues.length;i++) { // Get current value var curValue= dataValues[i]; // If value is not blank if (curValue != null && curValue.length > 0 ) { // Create DIV element var newItem = document.createElement('div'); // Append current value as a text node newItem.appendChild(document.createTextNode(curValue)); // Set attributes newItem.setAttribute('class', 'autoCompleteItem'); // Finally append new element to container container.appendChild(newItem); } } // Set first item as the selected node cursor = 0; // Get first node var selectedNode = container.childNodes[cursor]; // If first node is equal to the first node, hide the selection list if (selectedNode.childNodes[0].nodeValue == curControl.value) { hideSelectionList(curControl, container); } else { // Highlight the first node highlightSelectedNode(container, selectedNode); } }

Auto-complete TagLib library definition entry

Listing 20 contains the auto-complete control's TagLib library definition entry (with embedded comments for a description of each attribute).

Listing 20. Auto-complete TagLib library definition entry

<tag> <name>autocomplete</name> <tagclass>com.testwebsite.controls.AutoCompleteTag</tagclass> <bodycontent>JSP</bodycontent> <info> Auto-complete/suggest form input fields based on a specified value. </info> <!-- Unique identifier for control --> <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Minimum string length before submitting asynchronous request --> <attribute> <name>minimumlength</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Maximum number of items to include in suggestion list --> <attribute> <name>maxcount</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Width of control --> <attribute> <name>width</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Value of control --> <attribute> <name>value</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Data Url for asynchronous call. A default Servlet has been created, but for greater flexibility, a Web Service or another Servlet can be specified--> <attribute> <name>dataurl</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Class that provides suggest value list for control (Used if dataUrl not specified --> <attribute> <name>providerclass</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag>

Build the cascading drop-down JSP TagLib control

Typically, business-line applications include selection lists whose values are dependent on other form fields (for example, a product name that depends on the product category).

Prior to Ajax and asynchronous Web programming techniques, you were forced to render all values to the Web page (typically in a JavaScript array) and to dynamically populate the values within JavaScript. The JavaScript arrays were either multidimensional or contained tokens, such as the | character to delimit cascading values. Alternatively, the entire page would be refreshed to retrieve the values for cascading selection lists. Neither of these approaches are appealing when you're dealing with large data sets or trying to build user-friendly Web applications. With Ajax and asynchronization techniques, you can now provide the same rich and intuitive user experience typically only found in desktop applications.

The following sections describe the steps to create the cascading drop-down control:

Create a value provider/Servlet interface that can be called from JavaScript. Create the JSP TagLib control to encapsulate everything in a control that can be put in any JSP page.

Create the value provider and interface (Servlet)

Similar to the value provider you created for the auto-complete control, you create a Servlet to return a JSON array containing the values. Value providers for cascading controls take a little more effort, because the requirements and data often require separate Servlets or Web services to apply the business rules. Alternatively, you can use embedded JSP TagLib controls (controls included in the body of another tag), but that complicates things slightly. Using separate Servlets provides greater flexibility in terms of returning data to the client. The values can be dependent on other form fields or other complex business rules that are defined in the Servlet.

Listing 21 shows the two value providers for the cascading drop-down controls. The first is the City value provider, which is dependent on the State value. The second value provider is for County, which is dependent on the State and City values. Both Servlets return JSON arrays and use the Location Data Provider (a DAL component). The code is similar to developing a value provider for the auto-complete control; the key difference is that you use separate Servlets to keep the implementation simple and flexible.

Listing 21. CityServlet Servlet

package com.testwebsite.servlets; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.testwebsite.bll.LocationService; public class CityServlet extends HttpServlet { private static final long serialVersionUID = 3231866266466404450L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String data = null; // Get parameters from query string String format = req.getParameter("format"); String cityName = req.getParameter("cityName"); String stateName = req.getParameter("stateName"); // If format is not null and it's 'json' if (format != null & format.equalsIgnoreCase("json")) { // Get city data based on state name and city name prefix data = LocationService.getCitiesAsJson(cityName, stateName); resp.setContentType("text/plain"); } // Write response // Get writer for servlet response PrintWriter writer = resp.getWriter(); writer.println(data); writer.flush(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } /** * @model * @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com) * */ public class CountyServlet extends HttpServlet { /** * */ private static final long serialVersionUID = 3231866266466404450L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String data = null; // Get parameters from query string String format = req.getParameter("format"); String cityName = req.getParameter("cityName"); String stateName = req.getParameter("stateName"); String countyName = req.getParameter("countyName"); // If format is not null and it's 'json' if (format != null && format.equalsIgnoreCase("json")) { data = LocationService.getCountiesAsJson(countyName, stateName, cityName); resp.setContentType("text/plain"); } // Write response // Get writer for servlet response PrintWriter writer = resp.getWriter(); writer.println(data); writer.flush(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } }

Create the JSP TagLib control

The cascading drop-down control works as follows:

The page is rendered with an empty SELECT control.

control. The user selects the SELECT control. When it receives focus, an asynchronous call is made to retrieve the values from the server.

control. When it receives focus, an asynchronous call is made to retrieve the values from the server. The server sends the JSON array of values back to the client.

The client dynamically populates the SELECT control with the values in the JSON array.

control with the values in the JSON array. After the user selects a value from the list and the control loses focus (upon the blur event), the controls that are dependent on the current field are cleared. This is done to preserve data integrity (if the State value is change, the City value likely won't be valid).

The <ajax:page/> control renders the common functions that are usable by all cascading drop-down controls on a page, and the <ajax:dropdown/> control renders the JavaScript specific for an individual control instance.

Render the SELECT control for the cascading drop-down control

The rendering of the SELECT control is straightforward. The event handlers for the onfocus and onblur events are rendered as shown in Listing 22.

Listing 22. Render the SELECT control for the cascading drop-down control

... /** * The getSelectControlHtml method returns the html code to render the drop down (html * select) control. * @return Html code for drop down (html select) control */ protected String getSelectControlHtml() { StringBuffer html = new StringBuffer(); // Render dropdown/select control html.append("<select id='"); html.append(this.getId()); // Render on focus event handler html.append("' onfocus='"); html.append(this.getId()); html.append("_onSelect(this)'"); // Render on change event handler html.append(" onChange='"); html.append(this.getId()); html.append("_onChange(this)'"); // Render css class if specified if (this.getCssclass() != null && this.getCssclass().length() > 0) { html.append(" class='"); html.append(this.getCssclass()); html.append("'"); } // Render width if applicable (not 0/default/auto-fit) if (this.getWidth() > 0) { html.append(" style='width:"); html.append(this.getWidth()); html.append("px'"); } html.append("/>"); return html.toString(); } ...

Event handlers for the cascading drop-down control

In the onSelect event handler, the values for the controls, from which the current control cascades, are retrieved, and the URL is built to send the asynchronous request to the server. Upon receiving the response, the SELECT tag is populated with values returned in the JSON Array using JavaScript (see Listing 23).

Listing 23. On-select event handler

function stateName_onSelect(curControl) { if(curControl.options.length > 0) { return; } clearOptions(curControl); // Set waiting message in control var waitingOption = new Option('Retrieving values...','',true,true); curControl.options[curControl.options.length]=waitingOption; // The dataUrl is built dynamically based on the cascadeTo control var dataUrl = '/TestWebSite/State?format=json&stateName=' + getSelectedValue('stateName'); // Initialize the XMLHttpRequest object initializeXmlHttpRequest(); // If initialization was successful if (req!=null) { // Set callback function req.onreadystatechange=stateName_onServerResponse; // Set status text in browser window window.status='Retrieving State data from server...'; // Open asynchronous server call req.open('GET',dataUrl,true); // Send request req.send(null); } }

The following things happen in the server-response handler CONTROL-NAME_onServerResponse , which is dynamically generated by the cascading drop-down control tag:

Ignore status changes unless Loaded

Notify the user if an error occurred during the asynchronous call

Get the current control, and clear all OPTION elements

elements Get the response data, and convert it to an array containing strings

Populate the SELECT control using the array

Listing 24 is dynamically rendered by the <ajax:dropdown/> control.

Listing 24. Server-response handler (generated dynamically by the control)

function cityName_onServerResponse() { // If not finished, then return if(req.readyState!=4) { return; } // If an error occurred notify user and return if(req.status != 200) { alert('An error occurred retrieving data.'); return; } // Get current control var curControl = document.getElementById('cityName'); // Clear options clearOptions(curControl); // Get response data var responseData = req.responseText; // Convert to array var dataValues=eval('(' + responseData + ')'); // Populate SELECT tag with OPTION elements populateSelectControl(curControl, dataValues);window.status=''; }

The populateSelectControl function, which is generated by the <ajax:page/> tag, adds a blank OPTION to the SELECT control, as well as an OPTION element for each value in the dataValues array. The dynamically generated code fragment is shown in Listing 25.

Listing 25. Populate the SELECT control

function populateSelectControl(curControl, dataValues) { // Append blank option var blankOption= new Option('','',false,true); curControl.options[curControl.options.length]=blankOption; // Iterate through data value array for (var i=0;i<dataValues.length;i++) { // Create option var newOption= new Option(dataValues[i],dataValues[i],false,false); // Add option to control options curControl.options[curControl.options.length]=newOption; } }

In the onChange event handler, all controls that are dependent on the current control are cleared (see Listing 26).

Listing 26. On-change event handler

function stateName_onChange(curControl) { // Array dynamically generated by the control var toList=['cityName','countyName']; // If no controls are dependent on this function, simply return if (toList == null || toList.length == 0) { return; } // Iterate through list of controls that are dependent on // the current control for (var i=0; i < toList.length; i++) { // Get current control name var curControlName = toList[i]; // Get current control var curToControl = document.getElementById(curControlName); // If control not found, then exit if (curToControl == null) return; // Clear the current control clearOptions(curToControl); } }

The clearOptions function, which removes all items in the parent SELECT control, is rendered in the <ajax:page/> control (see Listing 27).

Listing 27. On-change event handler

function clearOptions(curControl) { // If current control is null then exit if (curControl == null) { alert('Unable to clear control'); return; } // Check if control is already blank and return if it is if (curControl.options.length < 1) { return; } // Clear the options curControl.options.length = 0; }

Cascading drop-down TagLib library definition entry

Listing 28 shows the cascading drop-down control's TagLib library definition entry (with embedded comments for a description of each attribute).

Listing 28. Cascading drop-down TagLib library definition entry

<tag> <name>dropdown</name> <tagclass>com.testwebsite.controls.DropDownTag</tagclass> <bodycontent>empty</bodycontent> <info> Populates Drop Down control asynchronously cascading values. </info> <!-- Unique identifier for control --> <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Url for Value Provider --> <attribute> <name>dataurl</name> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Message displayed while retrieving values from Value Provider --> <attribute> <name>updatemessage</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- CSS class name --> <attribute> <name>cssclass</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Current control value--> <attribute> <name>value</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Comma separated list of control id from which the current control cascades --> <attribute> <name>cascadefrom</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Comma separated list of control id to which the current control cascades --> <attribute> <name>cascadeto</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Width of control --> <attribute> <name>width</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag>

Build the test Web pages

The next step is to build sample pages to test the Ajax-enabled controls. You'll test the <ajax:autocomplete/> control with the Create New Contact page, and you'll test the <ajax:dropdown/> control with the Create New Employee page.

Figure 9 shows the test Create New Contact page, which demonstrates how the auto-complete control looks from a user perspective.

Figure 9. Create New Contact page demonstrates how to use the auto-complete control

The JSP code for this test page is shown in Listing 29.

Listing 29. Sample page to demonstrate using the auto-complete control

<%@ page contentType="text/html; charset=ISO-8859-5" %> <%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%> <html> <head> <title>New Contact Information</title> <link href="core.css" rel="stylesheet" type="text/css" /> <ajax:page/> </head> <body> <div id="container"> <form> <div class="dialog"> <div class="dialogTitle"> Contact Information </div> <div class="contentPane"> <div style="font-weight:bold">First Name:</div> <div> <input type="text" id="firstName" size="40"/> </div> <div style="font-weight:bold">Last Name:</div> <div> <input type="text" id="lastName" size="40"/> </div> <div style="font-weight:bold">Address:</div> <div> <input type="text" id="streetAddress" size="40"/> </div> <div style="font-weight:bold">City:</div> <div> <ajax:autocomplete id="cityName" width="40" providerclass="com.testwebsite.bll.CityValueProvider"/> </div> <div style="font-weight:bold">County:</div> <div> <input type="text" id="countyName" size="40"/> </div> <div style="font-weight:bold">Zip Code:</div> <div> <input type="text" id="zipCode" size="40"/> </div> </div> <div class="buttonPane"> <input type="reset" /> <input type="submit" value="Save"/> </div> </div> </form> </div> </body> </html>

The City Name field is now Ajax enabled. As the user types text in the City Name field, suggestions are dynamically displayed, similar to Google's auto-suggest.

Create a new employee

Figure 10 shows the Create New Employee page, which illustrates the cascading drop-down control from a user perspective.

Figure 10. Test page demonstrating the cascading drop-down control

The JSP code for this page is shown in Listing 30.

Listing 30. Sample page to demonstrate using the cascading drop-down control

<%@ page contentType="text/html; charset=ISO-8859-5" %> <%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%> <html> <head> <title>New Employee</title> <ajax:page/> <link href="core.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="container"> <form> <table class="dialog" cellspacing="0" cellpadding="0"> <thead> <tr> <td class="dialogTitle" colspan="2"> Employee Information </td> </tr> </thead> <tbody> <tr> <td class="fieldLabel"> Last Name: </td> <td class="fieldValue"> <input type="text" id="lastName" size="40"/> </td> </tr> <tr> <td class="fieldLabel"> First Name: </td> <td class="fieldValue"> <input type="text" id="firstName" size="40"/> </td> </tr> <tr> <td class="fieldLabel"> Address: </td> <td class="fieldValue"> <input type="text" id="streetAddress" size="40"/> </td> </tr> <tr> <td class="fieldLabel"> State: </td> <td class="fieldValue"> <ajax:dropdown id="stateName" dataurl="/State" width="240" updatemessage="Retrieving State data from server..." cascadeto="cityName,countyName" /> </td> </tr> <tr> <td class="fieldLabel"> City: </td> <td class="fieldValue"> <ajax:dropdown id="cityName" dataurl="/City" updatemessage="Retrieving City data from server..." cascadeto="countyName" width="240" cascadefrom="stateName" /> </td> </tr> <tr> <td class="fieldLabel"> County: </td> <td class="fieldValue"> <ajax:dropdown id="countyName" dataurl="/County" updatemessage="Retrieving County data from server..." cascadefrom="stateName,cityName" width="240"/> </td> </tr> <tr> <td class="fieldLabel"> Zip Code: </td> <td class="fieldValue"> <input type="text" id="zipCode" size="40" /> </td> </tr> </tbody> <tfoot align="right" class="buttonPane"> <tr> <td colspan="2"> <input type="reset" /> <input type="submit" value="Save"/> </td> </tr> </tfoot> </table> </form> </div> </body> </html>

Conclusion

In this article, you learned a few asynchronous communication techniques and how you can add JSON and Ajax to business-line applications through reusable JSP TagLib controls. Business-line applications can significantly benefit from Ajax-based controls thanks to the improved user experience and more responsive and intuitive user interfaces. The code isn't tremendously complex; you just need to integrate the key blocks (JavaScript, CSS, and J2EE technologies) to build the Ajax-enabled JSP controls.

You can extend the controls further to do the following:

Support cascading to/from other types of controls (in addition to SELECT controls)

Add mouse-event handling to the auto-complete control

Add encoding and checking for asynchronous requests

Downloadable resources

Related topics