5.2. Building a Suggestion Field
Building the suggestion field is the most fun but also the most complicated part of the next sample application. Our form will look like Figure 5-2.
When you type "M," the application finds all the names in the database starting with the letter M; in this case, there are three. If you type "Ma," the selection displays only "Mary." Hitting Return then calls up the record for Mary, as shown in Figure 5-3.
This application takes a few shortcuts. The first is the lookup: it doesn't go to the database and look up the matching set of names for every character that's typed. We could implement it that way, but it wouldn't improve the application and it would require more requests to the database, which has the potential for creating efficiency problems.
Even though Ajax applications are more responsive than traditional web applications, it's easy to imagine users cursing an application that makes a round trip to the server with every character that is typed. Instead, this application loads all of the usernames at the beginning. This approach wouldn't be ideal if there were hundreds or thousands of users, but if the application is only pulling a small amount of data, this simpler, single-query design is more efficient. For a bigger database, you could trigger the query with onkeyup( ) and get the result set for the letters in the suggestion field.
The HTML code for the form is presented in Example 5-4.
Example 5-4. The Ajax Customer Management code
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <LINK REL="stylesheet" TYPE="text/css" HREF="oreillyajax.css"> <title>Ajax on Java Customer Management Page</title> <script language="JavaScript" src="oreillySuggest.js"></script> <script language="JavaScript"> window.onload = function ( ) { init("ajax_username"); } </script> </head> <body > <h1>AJAX CUSTOMER MANAGEMENT</h1>
<form name="form1" action="/ch05-suggest" method="get">
<label for="ajax_username">Username:</label> <input class="cm_label" type="text" id="ajax_username" autocomplete="off"> <br /><br />
<label class="cm_label" for="password">Password:</label> <input class="cm_input" type="text" id="password" name="password"><br /><br />
<label class="cm_label" for="confirmpassword">Name:</label> <input class="cm_input" type="text" id="name" name="name"><br /><br />
<label class="cm_label" for="email">Email:</label> <input class="cm_input" type="text" id="email" name="email"><br /><br />
<label class="zlabel" for="address">Address:</label> <input class="cm_input" type="text" id="address" name="address"><br /><br />
<label class="cm_label" for="zipcode">Zip Code:</label> <input class="cm_input" type="text" id="zipcode" name="zipcode"><br /><br />
<label class="cm_label" for="state">State:</label> <input class="cm_input" type="text" id="state" name="state"><br /><br />
<label class="cm_label" for="city">City:</label> <input class="cm_input" type="text" id="city" name="city"><br /><br />
</form>
<a href="index.html">Sign up</a>
</body> </html>
|
A Cascading Style Sheet is used to set up the div that is needed for the suggestion field. Here's the code from oreillyajax.css:
div.suggestions { position: absolute; -moz-box-sizing: border-box; box-sizing: border-box; border: 1px solid blue; }
div.suggestions div { cursor: default; padding: 3px; }
div.suggestions div.current { background-color: #3366cc; color: white; }
|
A div (short for division) is an HTML tag for a generic block object to which formatting can be applied.
|
|
The suggestion div is set up for absolute positioning; that is, it is positioned according to fixed coordinates. Those coordinates are set in the oreillySuggest.js JavaScript file, based on the location of the username input field. So, the div's initial values are set in the CSS file, but the final values are set by the JavaScript functions. This strategy gives the program flexibility to adapt to changes in the HTML: the div remains anchored to the bottom of the username input field, even if that field moves around as the application grows. Figure 5-4 shows how the suggestion field looks on the screen.
Now the JavaScript file, oreillySuggest.js, comes into play. This file is loaded and the init( ) function is called to set up the ajax_username field:
window.onload = function ( ) { init("ajax_username"); }
The init( ) function does a lot of the groundwork: it requests the list of usernames from the server; sets up handlers for trigger events such as onkeyup, onkeydown, and onblur; and sets up the div for displaying suggestions. We'll look at these three tasks next. Here's the code for the init( ) function:
function init(field) { inputTextField = document.getElementById(field); cursor = -1; createDebugWindow( ); fillArrayWithAllUsernames( ); inputTextField.onkeyup = function (inEvent) { if (!inEvent) { inEvent = window.event; } keyUpHandler(inEvent); } inputTextField.onkeydown = function (inEvent) { if (!inEvent) { inEvent = window.event; } keyDownHandler(inEvent); } inputTextField.onblur = function ( ) { hideSuggestions( ); }
createDiv( ); }
5.2.1. Retrieving the Usernames
The init( ) function loads the usernames by calling fillArrayWithAllUsernames( ), which sets up the call to the server:
function fillArrayWithAllUsernames( ) { var url = "/ajax-customer-lab5-1/lookup?username=*"+"&type="+ escape("3");
if (window.XMLHttpRequest) { req = new XMLHttpRequest( ); } else if (window.ActiveXObject) { req = new ActiveXObject("Microsoft.XMLHTTP"); } req.open("Get",url,true); req.onreadystatechange = callbackFillUsernames; req.send(null); }
Nothing really new is happening here. We create a URL, get an XMLHttpRequest object to communicate with the server, register callbackFillUsernames( ) as the callback function, and send the URL to the server. The URL /ajax-customer-lab5-1/lookup?username=*&type=3 passes control to the AjaxLookupServlet. type=3 tells the servlet to call getAllUsers( ), which executes a wildcard lookup on the USERS table and returns a list of all usernames in the database:
private String getAllUsers( ) { Connection con = DatabaseConnector.getConnection( ); ResultSet result = null; StringBuffer returnSB = null; try { Statement select = con.createStatement( ); result = select.executeQuery("SELECT USERNAME from USERS;"); returnSB = new StringBuffer( ); while (result.next( )) { returnSB.append(result.getString("username") + ","); } returnSB.deleteCharAt(returnSB.length( ) - 1); catch (SQLException e) { // you could pop up a window with Ajax to let users know // there is a problem } finally { if (con != null) { try { con.close( ); } catch(SQLException e) { } } } return returnSB.toString( ); } }
The usernames are then collected into one long comma-separated string, using a StringBuffer. The resulting string is then sent back to the client as is: no XML, no JSON wrapping. It's simple and effective.
When the server returns the data, the browser calls callbackFillUsernames( ):
function callbackFillUsernames( ) { if (req.readyState==4) { if (req.status == 200) { populateUsernames( ); } } }
The next step, the call to populateUsernames( ), occurs only if the request has reached the ready state (i.e., the server has returned a result) and the request's status is 200 (success). populateUsernames( ) parses out the usernames from a comma-separated string and loads them into a JavaScript array. The String.split( ) JavaScript function does the conversion:
function populateUsernames( ) { var nameString = req.responseText; debugInfo('name array'+nameString); var nameArray = nameString.split(',');
lookAheadArray = nameArray; }
At this point, the lookAheadArray is loaded with usernames; the program is ready to interpret the characters entered into the username field and open a div if there are any matches.
5.2.2. Creating the Div
init( )'s final act is to call createDiv( ), which sets up the div:
function createDiv( ) { suggestionDiv = document.createElement("div"); suggestionDiv.style.zIndex = "2"; suggestionDiv.style.opacity ="0.8"; suggestionDiv.style.repeat = "repeat"; suggestionDiv.style.filter = "alpha(opacity=80)"; suggestionDiv.className = "suggestions"; suggestionDiv.style.visibility = "hidden"; suggestionDiv.style.width = inputTextField.offsetWidth; suggestionDiv.style.backgroundColor = "white"; suggestionDiv.style.autocomplete = "off"; suggestionDiv.style.backgroundImage = "url(transparent50.png)"; suggestionDiv.onmouseup = function( ) { inputTextField.focus( ); } suggestionDiv.onmouseover = function(inputEvent) { inputEvent = inputEvent || window.event; oTarget = inputEvent.target || inputEvent.srcElement; highlightSuggestion(oTarget); } suggestionDiv.onmousedown = function(inputEvent) { inputEvent = inputEvent || window.event; oTarget = inputEvent.target || inputEvent.srcElement; inputTextField.value = oTarget.firstChild.nodeValue; lookupUsername(inputTextField.value); hideSuggestions( ); debugInfo("textforLookup"+oTarget.firstChild.nodeValue); } document.body.appendChild(suggestionDiv); }
Most of this code sets various properties of the div, including:
zIndex
The depth. A higher number places it on top of an element with a lower number, so our div, which has a zIndex of 2, will appear on top of a div with a zIndex of 0 or 1.
opacity
The transparency. This controls the extent to which the elements below it show through. A value of 1 allows nothing through, while a value of 0 effectively makes the element invisible.
visibility
The visibility. The value hidden removes the element from the view. We can use this setting to hide the suggestion box when it is empty and show it when there is a match.
autocomplete
When autocomplete is set to off, the browser does not offer suggestions. This must be set to off, or the browser will drop down its own suggestion box!
onmouseup, onmouseover, etc.
JavaScript functions that are called when events occur.
For a full list of the properties that can be set, refer to Cascading Style Sheets: The Definitive Guide, by Eric A. Meyer (O'Reilly).
Ajax techniques become even more portable and powerful when combined with style sheets and when using the DOM to modify the characteristics of the HTML page.
5.2.3. Handling the Events
Now we're looking at the heart of the JavaScript: the event handlers that actually make the suggestion field work. The first event we'll investigate is onkeyup. Back in init( ), we registered the keyUpHandler( ) function as an event handler to be called whenever a key-up event (i.e., when a key is pressed and released) occurs in ajax_username. The keyUpHandler( ) function looks like this:
function keyUpHandler(inEvent) {
var potentials = new Array( ); var enteredText = inputTextField.value; var iKeyCode = inEvent.keyCode; debugInfo("key"+iKeyCode);
if (iKeyCode == 32 || iKeyCode == 8 || ( 45 < iKeyCode && iKeyCode < 112) || iKeyCode > 123) /*keys to consider*/ { if (enteredText.length > 0) { for (var i=0; i < lookAheadArray.length; i++) { if (lookAheadArray[i].indexOf(enteredText) == 0) { potentials.push(lookAheadArray[i]);
} } showSuggestions(potentials); }
if (potentials.length > 0) { if (iKeyCode != 46 && iKeyCode != 8) { typeAhead(potentials[0]); } showSuggestions(potentials); } else { hideSuggestions( ); } } }
The keyUpHandler( ) function saves the current value of the input field in the enteredText variable and saves the last key pressed in iKeyCode. It then checks whether this key was valid; if so, it executes a loop that checks enteredText against the strings in the lookAheadArray. Strings matching the beginning of enteredText are saved in the array potentials. If there are potential matches, the suggestion div is displayed with the call showSuggestions(potentials). Otherwise, the suggestion div is hidden.
Other handlers come into effect when the div is shown. The program keeps track of mouseover, mousedown, up-arrow, and down-arrow events to highlight the selected item. For presses of the arrow keys and Return key, the onkeydown event works well. In init( ), we registered the keyDownHandler( ) function as the event handler to be called whenever a key-down event occurs. The code for the keyDownHandler( ) function follows:
function keyDownHandler(inEvent) {
switch(inEvent.keyCode) { /* up arrow */ case 38: if (suggestionDiv.childNodes.length > 0 && cursor > 0) { var highlightNode = suggestionDiv.childNodes[--cursor]; highlightSuggestion(highlightNode); inputTextField.value = highlightNode.firstChild.nodeValue; } break; /* down arrow */ case 40: if (suggestionDiv.childNodes.length > 0 && cursor < suggestionDiv.childNodes.length-1) { var newNode = suggestionDiv.childNodes[++cursor]; highlightSuggestion(newNode); inputTextField.value = newNode.firstChild.nodeValue; } break; /* Return key = 13 */ case 13: var lookupName = inputTextField.value; hideSuggestions( ); lookupUsername(lookupName); break; } }
The down-arrow and up-arrow keys change the highlighted element in the suggestion div. Pressing the up-arrow key decrements the cursor index, and retrieves the indexed node and highlights it. The contents of that node (a complete username) are then copied into the text field. The down-arrow key behaves similarly. The highlightSuggestion( ) function does all the highlighting work, as you'll see momentarily.
If the user presses Return, we do a lookup on the name that was selected and hide the suggestions, which are no longer needed. The code for the lookupUsername( ) function follows:
function lookupUsername(foundname) {
debugInfo('looking up :'+foundname); var username = document.getElementById("ajax_username"); var url = urlbase+"/lookup?username=" + escape(foundname)+"&type="+ escape("2"); alert('url submitting:'+url); if (window.XMLHttpRequest) { req = new XMLHttpRequest( ); } else if (window.ActiveXObject) { req = new ActiveXObject("Microsoft.XMLHTTP"); } req.open("Get",url,true); req.onreadystatechange = callbackLookupUser; req.send(null); }
5.2.3.1. Highlighting a suggestion
The highlightSuggestion( ) function is simple. Its argument is the node that's currently selected, which we want to highlight. The method loops through all the nodes in the div. When it finds a node that matches the selected node, it sets the CSS class name for the background to "current", which is a CSS style we've designed for setting the background color. Nodes that don't match have their background classes set to "", which gives them the default background. Here's code for highlightSuggestion( ):
function highlightSuggestion(suggestionNode) { for (var i=0; i < suggestionDiv.childNodes.length; i++) { var sNode = suggestionDiv.childNodes[i]; if (sNode == suggestionNode) { sNode.className = "current"; } else if (sNode.className == "current") { sNode.className = ""; } } }
The onmouseover and onmousedown events were set up back in the init( ) function. The onmouseover event highlights the element that fired the trigger by calling highlightSuggestion(sugTarget):
suggestionDiv.onmouseover = function(inputEvent) { inputEvent = inputEvent || window.event; sugTarget = inputEvent.target || inputEvent.srcElement; highlightSuggestion(sugTarget); }
The onmousedown event selects the current node. It looks up the information associated with the current username, populates the form with that information, and hides the suggestion box:
suggestionDiv.onmousedown = function(inputEvent) { inputEvent = inputEvent || window.event; sugTarget = inputEvent.target || inputEvent.srcElement; inputTextField.value = sugTarget.firstChild.nodeValue; lookupUsername(inputTextField.value); hideSuggestions( ); }
5.2.4. Configuring the Servlets
All that's left is the setup for the servlet, but that is really just a repeat of what we have done in previous chapters.
The web.xml file, shown in Example 5-5, sets up the servlets that this application uses, which are:
AjaxZipCodesServlet AjaxSignupServlet AjaxLookupServlet AjaxUsernameServlet
Example 5-5. The web.xml file for the Ajax customer application
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd"> <web-app> <servlet> <servlet-name>AjaxZipCodesServlet</servlet-name> <servlet-class> com.oreilly.ajax.servlet.AjaxZipCodesServlet </servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>AjaxZipCodesServlet</servlet-name> <url-pattern>/zipcodes</url-pattern> </servlet-mapping> <servlet> <servlet-name>AjaxSignupServlet</servlet-name> <servlet-class> com.oreilly.ajax.servlet.AjaxSignupServlet </servlet-class> <load-on-startup>4</load-on-startup> </servlet> <servlet-mapping> <servlet-name>AjaxSignupServlet</servlet-name> <url-pattern>/signup</url-pattern> </servlet-mapping> <servlet> <servlet-name>AjaxLookupServlet</servlet-name> <servlet-class> com.oreilly.ajax.servlet.AjaxLookupServlet </servlet-class> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>AjaxLookupServlet</servlet-name> <url-pattern>/lookup</url-pattern> </servlet-mapping> <servlet> <servlet-name>AjaxUsernameServlet</servlet-name> <servlet-class> com.oreilly.ajax.servlet.AjaxUsernameServlet </servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>AjaxUsernameServlet</servlet-name> <url-pattern>/username</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
|
We've now created a complete Ajax application with a database on the backend. The application does a lot of visual processing, using JavaScript to manipulate the DOM. Yes, the database access is old-school, but it's generic enough to allow you to plug in your own backend access: JDO, Hibernate, or whatever other technology you choose.
A lot can be done to improve this application. For example, the JavaScript is very flat; it could be improved using an object-oriented approach in which all the functions are associated with a JavaScript object created by the init( ) function. Still, this application is typical of what you'll do with Ajax.
Don't be afraid to play with the JavaScript. That is where most of the visual power lies. In the past, you would have had to spend time learning Swing or another graphical library, but JavaScript's ability to manipulate the browser's DOM tree makes it in many respects as powerful as the other graphics APIs out there.
|
No comments:
Post a Comment