This chapter covers
Type-ahead suggest is one of the main applications of Ajax that has helped put Ajax into the mainstream world of programming. Google Suggest amazed people as they discovered a list of choosable suggestions appearing while they were typing (and even now, several months later, it still gives a buzz). It is as if a little helper is sitting next to you and telling you what you are looking for. Some people first thought they were storing values on the page, and others thought they were using a hidden IFrame to get the data. Google was actually using the XMLHttpRequest object to obtain the values for each keystroke.
In this chapter, you’ll learn how to send Ajax requests while the user is typing. We also examine the flaws of the available type-ahead suggest implementations and find ways to avoid these pitfalls in our application. At first we take a low-level approach that is easy to understand. After we get our application functioning, we reexamine it at a higher level, and use an object-oriented approach to gain more flexibility and usability. Before we build the application, though, let’s take a quick look at some of the typical type-ahead suggest features, and how we will design our application to use the best of those features.
Since Ajax has become popular, the type-ahead suggest has been one of the most sought-after pieces of code. Many people have created different versions of the type-ahead suggest that handle the interaction with the server in many ways. A lot of the solutions out there have flaws, while others go way overboard. We first evaluate some of the functionality in many type-ahead suggest applications and then take a quick look at Google Suggest. After that, we’ll design our application.
Numerous type-ahead applications are available, from basic to advanced. Each of them does the same basic thing; some have fancy interfaces with fading transition effects. If you do a search for “type-ahead suggest Ajax,” you will find plenty of examples.
If you look at a few of them, you should see that they all perform the same type of actions:
However, there are things that some of the scripts do not handle well. Developers need to take into consideration bandwidth, server capacity, and the client’s configuration. If you forget about these factors, Ajax may hurt your user’s experience instead of improving it.
The problems listed in table 10.1 are very common with Ajax applications.
Problem |
Result |
---|---|
Posting back on every keystroke | Large bandwidth consumption. Imagine the server-side bandwidth required for 1,000 users typing 10-characters words. |
Not caching data | Requests are hitting the database each time even though they are returning a subset of data they already have. |
The 56K modem | Yes, there are still people who dial in, and these users may see a lag in response time. |
Returning too many results | Not limiting the results can send too much data to the client, slowing down response time. |
Too fancy an interface | Fancy interfaces can be bad if they take a long time to render. |
Fast typists | Some people can type fast. Certain type-ahead scripts have problems if the user is not a hunt-and-peck typist. |
Many developers tend to forget about bandwidth. Even one user who is filling in a simple word can post back a number of times. Combine this with a fast typist, and you can have more hits in a second than you normally would for the user’s entire session in a non-Ajax application.
Keeping the user interface responsive is also very important. The more time a control takes to render, the longer the user has to wait before seeing it, and the less effective the type-ahead suggest is. Delays can also be introduced by hitting the server too hard. If requests are made too frequently, or if they return too much data, the responsiveness of the user interface will suffer.
A good strategy for improving responsiveness is to cache the data on the client. (We can also cache data on the server, but that’s another issue, more familiar to developers of classic web apps.) A type-ahead suggest system will typically return fewer results as extra characters are typed, and these will often be a subset of earlier results. A simple implementation will discard previous requests and fetch all data from the server every time. A more intelligent solution might retain the results of the initial request and whittle away unwanted entries as the user types, refreshing the user interface without going back to the server for every keystroke. This improves the application by increasing responsiveness and reducing the bandwidth. We’ll just be going through the result set that we have, making it quicker to pull the necessary information and eliminating the extra postbacks to the server.
That’s enough of the theory for now. In the next section, we’ll take a look at a production-ready implementation of the type-ahead suggest.
Some people consider Google Suggest to be the cream of the crop of the type-ahead suggest applications. Google Suggest is fast and user-friendly and does its job efficiently. In figure 10.1, Google Suggest is giving suggestions for the letter e.
In figure 10.1, the result set for the letter e is limited to 10 results. Knowing the vast collection of data Google has, it could be billions of results. Google uses an algorithm to determine what should be displayed. Developers have dissected Google’s JavaScript code to figure out how it is accomplishing the task. Remember, JavaScript code cannot be completely hidden from view, although obfuscation can help.
One of the impressive things about Google Suggest is that it accounts for fast typists by using timers to limit multiple postbacks in a short span of time. This had to be one of Google’s biggest concerns since they have such a large user base. Lots of postbacks to their servers could lead to problems, and limiting the number of postbacks saves resources.
Google has inspired us (and many others). In the next section, we’ll incorporate the best of the features we’ve reviewed as we design our own type-ahead Ajax application.
The type-ahead for this application will try to limit the impact on the server as much as possible. Smaller sites cannot handle the traffic that Google, Amazon, and Yahoo! can, since they do not have fancy load-balancing and multiple servers to handle the requests. Therefore, the more bandwidth that can be saved, the cheaper it is for the smaller websites in the long run.
To do this, we use the server only when we need new data. In this application, we use a script-centric interaction between the server and the client. The server formats the results as JavaScript statements that can be executed on the client. The data returned by the server contains only the text to display to the user and any identification field we want to relate with this data. This identification field can be compared to the value or an option tag. Most scripts do not allow us to send an ID; this one allows that. Then the browser handles the returned data, as JavaScript. With the data in the form of JavaScript variables, DHTML takes its turn in the process. You can see a diagram of this process in figure 10.2.
The Ajax portion of the script, as shown in figure 10.2, allows us to grab the data from the server. The server returns a text document containing a JavaScript statement to the client. The client processes the data contained in the JavaScript statement and checks to see if there are any results. If there are, the options are displayed for the user to choose.
The concept sounds easy until you realize that a large amount of JavaScript is involved. But with a number of small steps, the process of building a type-ahead script that minimizes the impact on the server is rather simple. The easiest part of this project is the server-side code, so that’s a good place to start.
The type-ahead suggest that we are about to tackle involves three parts: the server, the database, and the client. The database could actually be an XML file, but the same basic concept can be applied.
The server and the database code can be handled at the same time since we are just going to connect to a database of information. In this example, we will use Microsoft’s Northwind database and obtain the data from the Products table, but you can make this work for any database you want.
The idea behind the XMLHttpRequest object is to be able to send a request from the client to the server and receive back a formatted data structure. In this case, we create a text document dynamically with the data that we obtained from a database query. The text document will hold JavaScript code to create an array containing the data. You can see the steps for building the JavaScript array in listing 10.1.
The code in listing 10.1 lets us receive the values from the client and process the data into a string forming a JavaScript array. This newly created array is returned to the client where it will be processed. We need to initialize this on the page load of the document. The first step when we return the string is to make sure that the content type of the page is set to text/html .
The client-side code will post the values to this page via the XMLHttpRequest object. Therefore, we need to request the form element q for the text we are supposed to query . Unlike most type-ahead suggests, we’ll allow users to find results in the middle of a word, so we declare a string to handle this situation. The client script passes the boolean string within the form element named where. If it is true, we add a % to the start of our search term to enable searching from anywhere in the string.
We can now build the SQL statement to obtain the values from the database based on user input. To minimize the impact on the user, we limit the search by only allowing 15 records to be returned. We can then initialize our procedure to query the database and return a data table with the available search results.
Once the results have been returned from the database, we can start building the JavaScript array . We then loop through our record set , building the two-dimensional array containing the product name and ID. When the looping is complete, we write our string to the page so that our JavaScript statement can use it.
If you are using a server-side language that has a code-behind page and an HTML page, you need to remove all the extra tags from the HTML page. With our C# page, the only tag that should be on the ASPX page is the following:
<%@ Page Language="c#" AutoEventWireup="false" Codebehind="TypeAheadXML.aspx.cs" Inherits="Chapter10CS.TypeAheadXML"%>
If we did not remove the extra HTML tags that are normally on the ASPX page by default, we would not have a valid text (with JavaScript) file, meaning that our JavaScript DOM methods would not be able to use the data. To ensure that the data being transferred back to our client is correct, we need to run a quick test.
It is important to test the server-side code when you are working with Ajax since JavaScript is known for its problems, the causes of which are hard to find. Improvements have been made with Mozilla’s JavaScript console, but it is always a good idea to make sure that the server is performing properly to eliminate the chances of error.
We have two options for testing the server-side code. Since we will be using the HTTP POST method with our XMLHttpRequest object, either we have to create a simple form with two textboxes and submit it to the server-side page, or we can comment out the lines that check for the form submission and hard-code values in its place. As you can see in the partial code listing in listing 10.2, the form request statements are commented out and replaced with hard-coded values.
//string strQuery = Request.Form.Get("q").ToString(); string strQuery = "a"; string strAny = ""; //if (Request.Form.Get("where").ToLower() == "true") //{ strAny = "%"; //}
This code is looking for all the words that contain the letter a. Therefore, when this code is executed, the JavaScript array declaration appears as shown in figure 10.3.
As you can see, the string in figure 10.3 is correct. Therefore, we can remove the comments and hard-coded values and continue building the type-ahead suggest. You may be wondering where all the caching of the data returned from the server is. The client-side code will handle this. The only time the server will be called is for the very first keystroke or when the results are greater than or equal to 15. There is no reason to keep requesting the same data if we are only going to get a subset of the returned results. Now that we’ve finished the server-side code, let’s develop the client-side framework.
The client-side framework involves Ajax’s XMLHttpRequest object and a good amount of DHTML. The first thing we tackle is building the textboxes.
The HTML we’ll use is very simple since we’re dealing with only three form elements: two textboxes and a hidden field. The first textbox is the type-ahead suggest form element. The hidden field accepts the value of the selected item that the user picks from our type-ahead suggest. The other textbox does nothing other than keep our form from posting back to the server when the Enter key is pressed. The default action of the Enter key in a form with one text field is to submit the form. Adding another textbox to the page is the easiest way to get around the default action of the form. If you’re adding the type-ahead suggest on a page that contains multiple form elements, then you don’t need to add it. The basic HTML layout is shown in listing 10.3.
<form name="Form1" AUTOCOMPLETE="off" ID="Form1"> AutoComplete Text Box: <input type="text" name="txtUserInput" /> <input type="hidden" name="txtUserValue" ID="hidden1" /> <input type="text" name="txtIgnore" style="display:none" /> </form>
In listing 10.3, we added a form with autocomplete turned off. We need to do this to prevent the browser from putting values into the fields when the page is first loaded. It is a great feature when you need it, but in this case it disrupts the flow for our type-ahead suggest. Note that this is an Internet Explorer–specific fix, to prevent the built-in autocomplete drop-downs from interfering with our DHTML drop-down. Other browsers will ignore this attribute.
We added a textbox with the name txtUserInput, a hidden element with the name txtUserValue, and our dummy textbox with the name txtIgnore. The txtIgnore textbox, used to prevent automatic submission of the form, also has a CSS style applied to it to hide it from view, so the user cannot see it. There are other ways around this with coding, but this is the easiest and quickest solution. Now that we have added our text fields to the form, we can start coding the JavaScript.
The JavaScript for the type-ahead suggest performs three main tasks:
Before we start coding, it’s a good idea to see exactly what we’re going to be coding in action.
When the user types a letter, a hidden span is made visible with the information that relates to the typed letter. In figure 10.4, the highlighted letter in all of the available options is the letter a, which appears in the textbox also. The first option in the list is highlighted. By pressing the Up and Down Arrow keys, we can move this selection. Pressing the Enter key allows us to select the option. We can also select the option by clicking on one of the words from the list with the mouse.
Because of the complexity of this script, the explanation may seem rather jumpy, since it involves the use of many functions to perform the type-ahead suggest. One function monitors the keystrokes, another one loads the text and JavaScript code, a third one builds the list, a fourth one underlines the typed letters, and so on. You can download the code from Manning’s website so you can follow along and look at the code in your favorite editor.
To add Ajax functionality to this application, we must include the external JavaScript file, net.js (introduced in chapter 3), in the head tag. It contains the ContentLoader object, which allows us to initiate the Ajax request without having to do all the if-else checking:
<script type="text/javascript" src="net.js"></script>
To add the external file, we add the JavaScript tag and include the src attribute that specifies the external file. We link to the file just as we would link to an image or CSS file. This file does all the work of determining how to send the information to the server, hiding any browser-specific code behind the easy-to-use wrapper object. This now allows us to send and retrieve the data from the server without refreshing the page. With this file attached to our project, we can start to develop the type-ahead suggest.
Figure 10.4 shows a gray box that contains all the available options. The box is an HTML span element that is dynamically positioned to line up directly under the textbox. Instead of having to add the span to the page every time we want to use this script, we can add the span to the page from the script.
In listing 10.4, we create a new span element with DOM on the page load event. We are inserting a span to the HTML page with an ID of spanOutput and a CSS class name of spanTextDropdown. The span is then added by appending the new child element to the body element. The CSS class reference that we added allows us to assign the rules so that we can position the span dynamically. Since we are going to be dynamically positioning the span on the screen depending on where the textbox is located, we set the CSS class of the span to absolute positioning.
window.onload = function(){ var elemSpan = document.createElement("span"); elemSpan.id = "spanOutput"; elemSpan.className = "spanTextDropdown"; document.body.appendChild(elemSpan); }
We are using the page onload event handler to allow us to dynamically add a span element to the page. This prevents us from having to manually add it to the page every time we want to use this script. The DOM method createElement is used to create the span. We then need to assign our new span an ID and a className attribute. Once we add those new attributes, we can append the element to the page. At this point, let’s create our CSS class (listing 10.5) so that we can dynamically position the element on the page.
span.spanTextDropdown{ position: absolute; top: 0px; left: 0px; width: 150px; z-index: 101; background-color: #C0C0C0; border: 1px solid #000000; padding-left: 2px; overflow: visible; display: none; }
The position of the span is initially set to arbitrary positions on the screen by adding the top and left parameters. We set a default width for our span and set the z-index to be the uppermost layer on the page. The CSS rule also lets us style the background and border of our span so it stands out on the page. The display property is set to none so that it is hidden from the user’s view when the page is initially loaded. As the user starts to input data in the type-ahead text field, the display property is changed so that we can see the results.
Because we may want to use the type-ahead functionality on multiple fields, we should develop a way to have different properties assigned to the various elements. The properties are used to determine how the script reacts. We set properties to match text with case sensitivity, match anywhere in the text, use timeouts, and perform other features we will discuss shortly. One way to do this is to build an object that contains all the needed parameters that are unique to the individual textbox. Therefore, when we have the textbox in focus, we can reference the object that is attached to the element to obtain the correct settings. In listing 10.6, a new object is created so we are able to organize the list of parameters that we assign to the textbox.
function SetProperties(xElem,xHidden,xserverCode, xignoreCase,xmatchAnywhere,xmatchTextBoxWidth, xshowNoMatchMessage,xnoMatchingDataMessage,xuseTimeout, xtheVisibleTime){ var props={ elem: xElem, hidden: xHidden, serverCode: xserverCode, regExFlags: ( (xignoreCase) ? "i" : "" ), regExAny: ( (xmatchAnywhere) ? "^" : "" ), matchAnywhere: xmatchAnywhere, matchTextBoxWidth: xmatchTextBoxWidth, theVisibleTime: xtheVisibleTime, showNoMatchMessage: xshowNoMatchMessage, noMatchingDataMessage: xnoMatchingDataMessage, useTimeout: xuseTimeout }; AddHandler(xElem); return props; }
The first step in creating our objects for the type-ahead suggest is to create a new function called setProperties(), which can assign properties to the object. In this example, we are going to be passing in several parameters to this function. The list of parameters includes the textbox that the type-ahead is assigned to, the hidden element used to hold the value, the URL to the server-side page, a boolean to ignore case in the search, a boolean to match the text anywhere in the string, a boolean to match the textbox width, a boolean to show no matching message, the message to display, a boolean to determine if the options should hide after a given period of time, and the time span it should remain open.
This is a large list of parameters to pass into the function. We must take these parameters and assign them to our object. To do this, we use the JavaScript Object Notation (JSON), which we describe in more detail in appendix B. The keyword is defined before the colon, and the value afterward. Our treatment of two parameters, ignoreCase and matchAnywhere, is slightly more complex. Instead of storing the boolean value, we store the regular expression equivalent in the property. In this case, we use i to ignore case and ^ to match the beginning of a string in regular expressions. It is easier for us to set the regular expression parameters here instead of using if statements each time the functions are called.
The last step in our function is assigning the event handlers to the textbox. For this example, we’ll call a function that adds the event handlers automatically. We develop the code for the function in a moment, but first let’s call the function SetProperties() to create our object. The code in listing 10.7 is executed on the page onload event handler, enabling us to set the properties to the textbox.
window.onload = function(){ var elemSpan = document.createElement("span"); elemSpan.id = "spanOutput"; elemSpan.className = "spanTextDropdown"; document.body.appendChild(elemSpan); document.Form1.txtUserInput.obj = SetProperties(document.Form1.txtUserInput, document.Form1.txtUserValue,'typeAheadData.aspx', true,true,true,true,"No matching Data",false,null); }
The event handlers must be assigned when the page is loading. Therefore, we need to assign them to the window.onload event handler that we created earlier to add the new span element. In this example, we are using just one textbox for the type-ahead. We must reference the form element to which we want to add the type-ahead suggest and add a new property to it called obj. We will assign our custom object to this property so we can reference it throughout the script to obtain our values instead of using global variables.
We set the reference equal to the function SetProperties(). We then assign all the parameters that we created in listing 10.6. The important things to point out are that we are referencing the two form elements we created in listing 10.3 and we are calling the server-side page typeAheadData.aspx, which we created in listing 10.1. Now that the onload handler is initializing the process, we can add the event handlers, which our function SetProperties() is calling.
In order for us to determine the user’s actions within the textbox for the type-ahead suggest, we need to add event handlers to the form. The two main things to consider are the user’s typing on the keyboard and whether the user has left the text field. In listing 10.8, we use event handlers to detect the user’s actions.
var isOpera=(navigator.userAgent.toLowerCase().indexOf("opera")!= -1); function AddHandler(objText){ objText.onkeyup = GiveOptions; objText.onblur = function(){ if(this.obj.useTimeout)StartTimeout(); } if(isOpera)objText.onkeypress = GiveOptions; }
Listing 10.8 begins with a browser-detection statement. The browser detection is going to be used in a few places in this example since Opera behaves differently with keypress detection. This is the easiest way to determine if the browser used is Opera, but it is not always the most reliable way since Opera can act like other browsers.
Our function AddHandler() is given a reference to the textbox. This reference allows us to add the onkeyup and onblur event handlers to the element. The onkeyup event handler fires a function called GiveOptions() when the key is released on the keyboard. Therefore, when the user types a five-letter word, the function GiveOptions is fired five times as the keys are released.
The onblur event handler that we attach to our textbox calls the function StartTimeout() when the user removes the focus from the textbox. Actions that can remove the focus from the textbox include clicking on another part of the screen or pressing the Tab key. We will be developing the StartTimeout() function in listing 10.19.
The reason we did the browser detection for Opera is that it does not fire the onkeyup event handler in the same manner as the other browsers do. When onkeyup is fired, Opera does not show the value in the textbox that includes that current keystroke. Adding the onkeypress event handler to Opera corrects this problem. You can see that we check for the browser using our boolean variable isOpera, and we then assign our onkeypress event handler to our textbox. With this event handler, Opera performs in the same way as other browsers. Since we now are able to detect the user’s typing, we can determine what actions need to take place in the function GiveOptions().
The GiveOptions() function that we are about to create is called when keypress events are fired. This function has two main jobs: determining the action to take depending on the keystroke, and determining whether we need to use Ajax to obtain the data from the server or use the data we already have. Therefore, the GiveOptions() function is performing the same role as the data caching that we discussed in section 10.1.1. By using client-side code to handle the additional keystrokes, we are decreasing the bandwidth consumption of the type-ahead suggest. To implement our cache of options, let’s set some global variables on the client. The code in listing 10.9 contains a list of global variables that we need to start with.
var arrOptions = new Array(); var strLastValue = ""; var bMadeRequest; var theTextBox; var objLastActive; var currentValueSelected = -1; var bNoResults = false; var isTiming = false;
The first global variable is arrOptions. This variable references an array that holds all the available options from the server query. The next variable is strLastValue, which holds the last string that was contained in the textbox. The variable bMadeRequest is a boolean flag that lets us know that a request has already been sent to the server so we do not keep sending additional requests. The flag is meant for very fast typists, so we do not have to worry about using timeouts as Google does.
The variable theTextBox will hold a reference to the textbox that the user has in focus, whereas objLastActive will hold the reference to the last active textbox. This is used to determine whether the data set needs to be refreshed if the user switches textboxes. While there is only one visible textbox on our example, if this solution is implemented on a window with multiple textboxes, we need to know which one has the focus. The next variable, currentValueSelected, will act like the selectedIndex of a select list. If the value is -1, nothing is selected. The final global variable that we need right now is a boolean bNoResults. This will tell us that there are no results, so we should not bother trying to find any. The variable isTiming allows us to determine whether a timer is running on the page. The timer runs to hide the options from the user’s view if there is a period of inactivity.
Even though you might not completely understand what these global variables’ roles are at this time, you’ll understand better when we start using them. With all our global variables referenced, we can build the GiveOptions() function, which is called from the keystrokes in the textbox. The GiveOptions() function in listing 10.10 lets us determine the action the user has performed in the textbox.
If the user is typing a word, either this function will start a new search, checking the server for matching data, or it will work with the cached result set. If we do not need to get new data from the server, then we can call a BuildList() function, which will limit the result set. We explain more about that in the section “Building the results span,” later in this chapter.
The GiveOptions() function is declared with the parameter e, which allows us to detect the source of the event. The first thing we need to declare is a local variable intKey. This variable holds the code of the key that the user pressed . To determine which key was pressed, we must determine what method the user’s browser needs to function. If the window.event property is supported, then we know the browser is IE. We use event.keyCode to obtain the key code value, and we also use event.srcElement to get the object of the user’s textbox. For the other browsers, we use e.which to obtain the key code value and e.target to obtain the textbox object reference.
We then need to check whether the textbox is using a timer to hide the textbox . To do so, we reference the textbox’s obj property (which we created earlier) and the boolean useTimeout. If the timer is running, we cancel it and then restart it by calling the functions EraseTimeout() and StartTimeout(), which we will code in the section “Using JavaScript timers.”
We then check to see if anything is in the textbox . If nothing is there, we call a HideTheBox() function (which is developed in the section “Setting the selected value”), set the strLastValue to null, and return false to exit the function. If the textbox contains text, then we can continue. Before we can detect the Enter key and arrow keys, we need to verify that the current active textbox is the same textbox as the last textbox that was active.
The first key to detect is the Enter key, which has a key code of 13 . The Enter key will allow us to grab the value of the selected drop-down item and place it into the visible textbox. Therefore, we call a GrabHighlighted() function (which we will also code in the section “Setting the selected value”). We then remove the focus from the textbox and exit the function.
The next two keys we want to capture are the Up and Down Arrow keys, which have the values 38 and 40, respectively. The arrow keys move the highlighted option up and down the list. In figure 10.4, the dark gray bar is the selected item. By using the Down Arrow key, you can select the next item in the list. This functionality will be discussed in the section “Highlighting the options.” The important thing to note is that the Down Arrow key sends a value of 1 to the function MoveHighlight(), while the Up Arrow key sends -1.
If no special key was pressed, then we check to see if we should hit the server to obtain the values or use the list that we already obtained from the server . Here again we are using the caching mechanism of this script to limit the postbacks and reduce the load on the server. We can perform a couple of checks to see if we have to get new results. The first check is to determine whether or not the last active textbox is the textbox that currently has the focus. The next check is to make sure that the text the user typed into the textbox is the same as last time with only an addition at the end. If there are no results or our result set has 15 elements or less, then we need to check the server for data. The last check is to make sure that the current value’s length is greater than the last value. If any of these checks pass, then we need to obtain new data from the server. We set the objLastActive with the current textbox. We then set a boolean saying that a request has been sent so we do not perform multiple requests, and we call our function TypeAhead() to grab the values.
Then we set the current string in the textbox to the last-known string . We’ll use that value again to see if we need to request data from the server on the next keystroke. This brings us to accessing the server to obtain the data.
The XMLHttpRequest object allows us to transfer the text from the textbox to the server and to receive the results from the server. In this case, we are posting the data to the server since the server-side page we created in listing 10.1 is referencing the elements submitted in a form. We must specify in our ContentLoader the location of the page on the server, the function to call when it is completed, and the form parameters to be submitted to the form, as shown in listing 10.11.
function TypeAhead(xStrText){ var strParams = "q=" + xStrText + "&where=" + theTextBox.obj.matchAnywhere; var loader1 = new net.ContentLoader( theTextBox.obj.serverCode, BuildChoices,null,"POST",strParams); }
When we called the function TypeAhead() from the function GiveOptions(), we passed the current string value from the textbox to perform the search. We need to build the parameter string, strParams, that contains our textbox string value and also the matchAnywhere boolean value. Both of these were used in listing 10.1 to develop the results. Then we start to load the document by calling the ContentLoader. We are sending the URL of the server-side page and the JavaScript function to call when the results are returned as the first two parameters in the ContentLoader. The third parameter is null since we want to ignore any error messages. Ignoring the errors allows the type-ahead to act like a normal text field. The last two properties inform the ContentLoader to post the data to the server and send the form parameters contained in the string strParams.
When the results are returned from the server, the function BuildChoices() is called to allow us to finish the processing of the data on the client. When we developed the server-side code, we returned the results as a two-dimensional JavaScript array. This array contained the option’s text-value pairs for the choices. However, in the response, it is just a string of characters. We need to take this returned string and make it accessible as a JavaScript array. Listing 10.12 contains the functionality that executes the information returned from our ContentLoader using the eval() method.
function BuildChoices(){ var strText = this.req.responseText; eval(strText); BuildList(strLastValue); bMadeRequest = false; }
The responseText property of the returned request object lets us obtain the text from the Ajax request. To allow this returned string to be used by our JavaScript code, we need to use the eval() method. The eval() method evaluates the string contained within its parentheses. In this case, it recognizes that the string is a variable declaration to make a new array. It processes the array so that we can access it. If we were just to write the string to the page, it would not be accessible to the JavaScript statement. Developers frown on using the eval() method since it is known to be slow. However, in this case we are eliminating the need to loop through an XML document on the client to obtain the values. Now we can call the function BuildList() to format and display the returned results. We also want to set our boolean bMadeRequest to false to inform the rest of the script that the request to the server is complete.
The use of JavaScript to manipulate the current document is normally considered to be DHTML. In this example, we are taking a two-dimensional array and turning it into lines of text on the screen. Looking back at figure 10.4, we see a list of words that have a portion of their text underlined. The underlined text is the text that matches what the user entered. We are going to display those words in the span element.
The BuildList() function that we create in listing 10.13 utilizes a series of three functions. The functions include finding the matched words, setting the position of the span, and formatting the results with the underline.
The function BuildList() in listing 10.13 takes the string the user entered and formats the results. The first thing we need to do is dynamically position the span element directly under the textbox from which the type-ahead is being implemented. To do this, we call the function SetElementPosition() (which we develop in the section “Dynamically setting an element’s position”). After we position the span element, we can start to manipulate the array to find the matches by using the MakeMatches() function (see the section “Using regular expressions”). This function returns an array that contains only the information that matches the user’s input. We are using JavaScript to limit the results on the client rather than requiring the processing to be done on the server like most of the type-ahead applications available online.
The MakeMatches() function formats the results and returns them as an array. We then turn this array into a string by using the join method. If the length of the string is greater than 0, then we can display the results in a span by setting its innerHTML property. Then we select the first element in the list and set its className so it is highlighted.
If the returned array contains no data, then we display our “no matches” message if the textbox allows it. We make sure that we set the currentSelectedValue to -1 so we know there are no matches. If no message is to be displayed, then we just hide the box.
We’ve finished the BuildList() function, so now we have to create all the functions that it calls. The first one we’ll tackle is SetElementPosition().
The input textbox is positioned on the page by the browser’s layout engine. When we construct the DHTML drop-down suggest, we want to place it exactly in line with the textbox. One of our most difficult tasks is finding the position of a nonpositioned element, in this case the textbox, so that we can compute the drop-down’s coordinates. A nonpositioned element is one that is relatively set on the page without specifying the absolute left and top positions. If we reference the left and top positions for our textbox, we’ll get an undefined string returned. Therefore, we need to use some JavaScript to determine the position of our element so that our box of choices lines up directly underneath it. In listing 10.14, we are dynamically positioning the span element to line up under our textbox.
function SetElementPosition(theTextBoxInt){ var selectedPosX = 0; var selectedPosY = 0; var theElement = theTextBoxInt; if (!theElement) return; var theElemHeight = theElement.offsetHeight; var theElemWidth = theElement.offsetWidth; while(theElement != null){ selectedPosX += theElement.offsetLeft; selectedPosY += theElement.offsetTop; theElement = theElement.offsetParent; } xPosElement = document.getElementById("spanOutput"); xPosElement.style.left = selectedPosX; if(theTextBoxInt.obj.matchTextBoxWidth) xPosElement.style.width = theElemWidth; xPosElement.style.top = selectedPosY + theElemHeight xPosElement.style.display = "block"; if(theTextBoxInt.obj.useTimeout){ xPosElement.onmouseout = StartTimeout; xPosElement.onmouseover = EraseTimeout; } else{ xPosElement.onmouseout = null; xPosElement.onmouseover = null; } }
In listing 10.14, we declare our function SetElementPosition(), which accepts one parameter: the textbox object reference. Two local variables, selectedPosX and selectedPosY, are set to 0. These two integers are used to calculate the position of the element. The textbox reference is set into another local variable. The textbox’s width and height are obtained by referencing the offsetHeight and offsetWidth properties.
A while loop is used to loop through the document tree. The document tree allows us to obtain the X and Y positions of the element relative to its parent. By looping through each positioned parent, we can find the exact position of the element by adding the offset position to the two local variables that we created.
Once we obtain the position of the textbox, we can retrieve the span’s object reference, which is used to set the left and top positions of the drop-down suggest element. We then look at the textbox’s obj object that we created to see if its width property is supposed to match the width of the textbox. If the boolean is true, then we set the width of the span. If the boolean is false, the width comes from the value specified in the stylesheet. The last step is to change the visibility of the span so it is not hidden from the user’s view any more. We do this by setting the display property to block.
Now that our span is adequately positioned and visible to the user, we can develop the code that fills in the selectable option’s content.
Since we are going to be searching for string segments, regular expressions are one of the best ways to find matches with added flexibility. The MakeMatches() function that we create next allows us to find the words in the options list that match the user’s text in the textbox. This means we can avoid a trip to the server after every keystroke, since the function narrows the choices for us. The code in listing 10.15 lets us save bandwidth by limiting our result set.
var countForId = 0; function MakeMatches(xCompareStr){ countForId = 0; var matchArray = new Array(); var regExp = new RegExp(theTextBox.obj.regExAny + xCompareStr,theTextBox.obj.regExFlags); for(i=0;i<arrOptions.length;i++){ var theMatch = arrOptions[i][0].match(regExp); if(theMatch){ matchArray[matchArray.length]= CreateUnderline(arrOptions[i][0], xCompareStr,i); } } return matchArray; }
We create the function MakeMatches(), which accepts one parameter: the string the user entered. We then reset the variable countForId to 0 and create a local array variable matchArray. (Note that countForId is a global variable. That keeps the example simple for now. We’ll do away with it in the refactoring section later!) The key to this function is creating a regular expression that finds the options that match the user’s input. Since we have already determined the parameters for the regular expression when we created the code in listing 10.6, we just need to reference our textbox’s object. We add the property reqExAny, which allows us to match at the beginning of or anywhere in the string. The regExFlags property lets us determine whether to ignore the case when performing the matches.
With the regular expression completed, we loop through the array arrOptions to verify that the options in the array match our regular expression. If they match, then we add the text to our array matchArray after we call the function CreateUnderline(). CreateUnderline() formats the code to be displayed.
After we loop through all the elements in our array, we return the matched options to the main function BuildList(), where the matches are displayed to the user. MakeMatches() provides the caching mechanism that we talked about earlier. Instead of returning to the server to limit the search for every keystroke, regular expressions allow us to limit the available options to the user. The CreateUnderline() function is the last step in formatting the results.
The final step for formatting the strings so that the user can view and interact with them is to manipulate the string so that it is contained within a span tag, has an underline under the matching text, and has the onclick event handler attached to it so we can select it with the mouse. This section uses regular expressions again to build the formatted string, as you can see in listing 10.16.
var undeStart = "<span class='spanMatchText'>"; var undeEnd = "</span>"; var selectSpanStart = "<span style='width:100%;display:block;' class='spanNormalElement' onmouseover='SetHighColor(this)' "; var selectSpanEnd ="</span>"; function CreateUnderline(xStr,xTextMatch,xVal){ selectSpanMid = "onclick='SetText(" + xVal + ")'" + "id='OptionsList_" + countForId + "' theArrayNumber='"+ xVal +"'>"; var regExp = new RegExp(theTextBox.obj.regExAny + xTextMatch,theTextBox.obj.regExFlags); var aStart = xStr.search(regExp); var matchedText = xStr.substring(aStart, aStart + xTextMatch.length); countForId++; return selectSpanStart + selectSpanMid + xStr.replace(regExp,undeStart + matchedText + undeEnd) + selectSpanEnd; }
In listing 10.16, we define two variables to hold strings that are used to insert a CSS class around the portion of text that matches the string. This allows us to style the text easily. The first variable, undeStart, holds our start span tag, while the second variable, undeEnd, holds the closing span tag.
The next two variables form the container for the entire string. This container lets us manipulate the background color and determine whether the cell is clicked on. You can see that in the variable selectSpanStart, we added a mouse-over to highlight the cell. The selectSpanEnd variable is just the closing tag for the span.
Our function CreateUnderline() is called by the MakeMatches() function that we just coded. MakeMatches() passes in three parameters: the string the user entered, the option’s text, and the option’s value. With the passed-in data, we can develop the onclick handler and add an ID for the span. The onclick handler allows us to select the option, and the ID allows us to use DOM to select the option from the list.
We use a regular expression again to match the text typed by the user. This is so that we can insert the underline spans we created in the string. The search method is used to determine where the match is located in the string. After we find the location of the string, we can obtain the substring so that we can keep the original formatting. Our counter countForId is incremented, and we return our formatted string by joining together all the span elements that we created. The returned text is now formatted, but we still need to finish the CSS classes we added to the span elements.
The span elements were assigned CSS class names, so we do not have to manually go into the JavaScript code to change certain properties of the text. This allows us to fit the autocomplete textbox into any color scheme by simply changing these few CSS rules:
span.spanMatchText{ text-decoration: underline; font-weight: bold; } span.spanNormalElement{ background: #C0C0C0; } span.spanHighElement{ background: #000040; color: white; cursor: pointer; } span.noMatchData{ font-weight: bold; color: #0000FF; }
Remember that in figure 10.4 the matching text was bold and underlined. You can see those two properties listed in the CSS rule span.spanMatchText. The span default style is represented with span.spanNormalElement, which contains a gray background color. The selected item is applied the CSS rule span.spanHighElement. By looking back at that figure you can see that the background color is dark gray and the text color is white. The cursor is also changed to a pointer, so the user knows she can select that option with the mouse. We can add more properties to any of the elements, such as fonts, sizes, borders, and so on. Now that we have built the stylesheet rules, we have finished working with outputting the results. All that is left is handling the Enter and arrow keys and creating our timer (which hides the options in case of inactivity).
Earlier in the chapter, we captured the keypresses of the Up and Down Arrow keys so that the user could move the selectedIndex up or down without having to use her mouse. The arrow keys send us either 1 (to move down the selection) or –1 (to move up the selection). When we move a selection, we apply CSS classes to the span elements. We are also adjusting the global variable currentValueSelected so that it holds our current index. The MoveHighlight() function in listing 10.17 gives us a richer user interface since it interacts with both the mouse and the keyboard.
function MoveHighlight(xDir){ if(currentValueSelected >= 0){ newValue = parseInt(currentValueSelected) + parseInt(xDir); if(newValue > -1 && newValue < countForId){ currentValueSelected = newValue; SetHighColor (null); } } } function SetHighColor(theTextBox){ if(theTextBox){ currentValueSelected = theTextBox.id.slice(theTextBox.id.indexOf("_")+1, theTextBox.id.length); } for(i = 0; i < countForId; i++){ document.getElementById('OptionsList_' + i).className = 'spanNormalElement'; } document.getElementById('OptionsList_' + currentValueSelected).className = 'spanHighElement'; }
The MoveHighlight() function enables the user to use the Up and Down Arrow keys to make a selection. The function accepts one parameter, xDir, symbolizing the direction in which the highlight should be moved. The first check verifies that we have options to select. If there are options, we can obtain the new value. We verify that the new value is within the range of the selection. If it is, we set currentValueSelected and proceed to the next function, SetHighColor(), to highlight the new selection.
SetHighColor() is called from two different events: the arrow keys and the onmouseover event handler. This function is called to remove the highlight from the last selected option and add it to the new option that has been chosen. The onmouseover event in listing 10.16 passes in the object of the span; therefore, we need to obtain the index number of the span by ripping apart the ID. The arrow keys pass this value, so we are not required to perform this action since the moveHighlight() function already set currentValueSelected.
We loop through all of the span tags and set their CSS class to spanNormalElement. This resets their appearance to their nonselected state. After the looping is completed, we add the CSS class to the selected option. With the two functions that we just created, we have given the user the ability to select an option with either the mouse or the keyboard. All that is left is to take this selected value and add it to the textbox.
The purpose of the type-ahead suggest is to allow the users to select available options to limit the amount of effort required to fill in a form field. In order to do this, we need to take the index of the item that the user selected and set the text to the textbox and the value to the hidden text field. These three functions in listing 10.18 allow our span element to act like an HTML select element.
function SetText(xVal){ theTextBox.value = arrOptions[xVal][0]; //set text value theTextBox.obj.hidden.value = arrOptions[xVal][1]; document.getElementById("spanOutput").style.display = "none"; currentValueSelected = -1; //remove the selected index } function GrabHighlighted(){ if(currentValueSelected >= 0){ xVal = document.getElementById("OptionsList_" + currentValueSelected).getAttribute("theArrayNumber"); SetText(xVal); HideTheBox(); } } function HideTheBox(){ document.getElementById("spanOutput").style.display = "none"; currentValueSelected = -1; EraseTimeout(); }
The function that allows us to obtain the text and value of the selected item is GrabHighlighted(). First we need to see if the user has selected a value. If a value is selected, then we obtain the index number of the arrOptions array in which the text resides. To do this, we grab the value from the attribute, theArrayNumber, that we set earlier. Then, we call the function SetText() to set the selected option’s text and value into their respective form elements.
SetText() uses the index value passed in as a parameter to index the array arrOptions. The visible text the user sees is set by indexing the first index of the array. The hidden form element receives the second index value stored in our array. After we retrieve the values, we remove the option list from the screen by calling our function HideTheBox().
HideTheBox() allows us to remove the span, spanOutput, from the view. To do this, we reference the span and set its style.display property to none. We remove the selected index by setting the variable currentValueSelected to –1. Any timers that we may have set are removed by calling EraseTimeout(), which we develop next.
This is the final JavaScript section before the type-ahead project is complete, so your brain may be hurting from all of this client-side code. The JavaScript’s setTimeout() method executes a statement after an elapsed time has passed. The elapsed time is specified in milliseconds, which we added to the object we created back in listing 10.6. The reason for using a timer is to hide the selection span if there is an inactive timeout period. If we set the parameter in our object useTimeout to true, then this function will be called. The timer in listing 10.19 gives us one more feature for a rich user interface.
function EraseTimeout(){ clearTimeout(isTiming); isTiming = false; } function StartTimeout(){ isTiming = setTimeout("HideTheBox()", theTextBox.obj.theVisibleTime); }
The function StartTimeout() sets the timer when the function is executed. We initialize the timer by setting the variable isTiming to the setTimeout method. The setTimeout method should call the function HideTheBox() after the set time span, indicated by theVisibleTime.
The only other thing we have to do is to remove the timeout. To cancel it, we create the EraseTimeout() function that uses JavaScript’s built-in clearTimeout() function for preventing HideTheBox() from firing. We set our boolean isTiming to false.
Upon finishing that last line of code, we can now run the type-ahead suggest project! Save the project, open it, and start typing in a word. Figure 10.5 shows the progression of the type-ahead suggest. The first letter, s, returned more than 15 options. The second letter, h, reduced the list to five options. The third letter, o, reduced the list to one, which we selected by pressing the Enter key. By adding this project to any form, you can increase the efficiency of your users so they do not have to type in entire words.
With the way that we designed the script, we can have multiple type-ahead select elements on the page. We just need to add declarations with new calls to SetProperties() for each element. The downside to this method is that in order to have different values fill in the drop-down, we would have to reference different server-side pages. In most cases we will be fine with this, but the only difference between these methods is most likely the SQL statement.
We can come up with an elaborate solution to this problem by adding an additional parameter to our custom object and sending it to the server. Another option is to work with what we have now so that we can make a minimum number of changes to our code. In this case, the simple solution involves changing one line in our code and adding an if statement on the server-side code.
The goal is to be able to somehow differentiate between the elements on the server to determine which element has caused the postback. A simple way to tell the difference is to use the name that is on the element. In this case, we’ll reference the name of our textbox. In listing 10.20, we alter the parameter string to allow for this new functionality.
function TypeAhead(xStrText){ var strParams = "q=" + xStrText + "&where=" + theTextBox.obj.matchAnywhere + "&name=" + theTextBox.name; var loader1 = new net.ContentLoader(theTextBox.obj.serverCode, BuildChoices,null,"POST",strParams); }
By making the slight change to the variable strParams in the function Type-Ahead(), we are passing the name of the textbox in the form parameters being passed to the server. That means we can reference this value on the server and use either an if-else or a case statement to run a different query. Now we do not need multiple pages for multiple elements.
Now that we’ve developed a fairly robust set of features for providing type-ahead suggest capabilities, it’s time to think about how to package all of this functionality in a more palatable way for the consuming web developer. What we’ve developed to this point provides the functionality needed for the suggest behavior, but it has some drawbacks in terms of the work required for a developer to plug it into a web page—or 20 to 30 web pages, for that matter.
So let’s imagine for a moment that we are the grand architect of an Ajax-based web framework and we’ve been assigned the task of writing a suggest component for the rest of the company to use. As the requirements-gathering meeting disperses, we’re handed a sheet giving us our loose set of functional requirements. Unsure of what we’re getting into, we glance down at the list (table 10.2).
Number |
Requirement Description |
Priority |
---|---|---|
1 | The component must work with existing HTML markup without requiring any changes to the markup. Simple changes to the head section to inject the component’s behavior are acceptable. | 1 |
2 | The component must support being instantiated multiple times on the same page with no additional effort. | 1 |
3 | Each component instance should be independently configurable, in terms of both the behavioral aspects (e.g., case matching, match anywhere) and the CSS styling. | 1 |
4 | The component should not introduce any global variables. The company uses third-party JavaScript libraries, and the global namespace is already cluttered. Any global names, with the exception of the component itself, are strictly prohibited. | 1 |
5 | The component should provide reasonable defaults for all of the configuration options. | 1 |
6 | The component must work in IE and Firefox. | 1 |
7 | The component should use an open source framework to reduce the amount of coding effort required and improve the quality and robustness of the solution. | 1 |
8 | Oh, and if you can, get it done by the end of the week. | 1 |
As we survey the list, several thoughts run through our head. Okay, first of all, the powers that be don’t seem understand the concept of a priority. But we’re fairly used to that, so we look to the heart of the matter—the requirements. And despite all our hard work, we’ve satisfied less than half of them. Our script is already done, so that satisfies number 7 in the sense that we don’t need to reduce the effort because the script is already implemented. Obviously requirement 8 is satisfied for the same reason. Our script supports multiple browsers, so number 6 is covered as well. As for the rest, we’ve got some work to do. We have only a week, so we’d better get started.
The first thing to decide is how to boost productivity to accommodate the short time schedule. One of the best ways to do this is by leveraging the work of others. If someone else can do some of the work, that’s less for us to do. So for this component, we’re going to leverage the open source efforts of Rico (http://openrico.org) and by extension Prototype.js (http://prototype.conio.net/). Rico provides some Ajax infrastructure, effects, and utility methods that will boost our development speed. Prototype provides some infrastructure for nice syntactic idioms that will make our code look cleaner and also take less time to develop. Let’s take a look at the implications of using Prototype and Rico.
Prototype provides developers with a few extensions to the core JavaScript object as well as a few functions that make for a nice coding style. Here are the ones we’ll use in this example:
The Class object introduced in the Prototype library has a single method called create(), which has the responsibility of creating instances that can have any number of methods. The create() method returns a function that calls another method in the same object named initialize(). It sounds complicated from the inside, but in practical use, it is straightforward. What this effectively does is create a syntactical way for specifying types in JavaScript. The idiom is as follows:
This segment of code creates what we can think of as a “class” (even though the language itself doesn’t support such a concept) and defines a constructor function named initialize(). The client of the component can create an instance via this line of code:
var textSuggest = new TextSuggest(p1, p2, p3);
The Prototype library extends the JavaScript base object and adds a method to it named extend(), thus making this method available to all objects. The extend() method takes as its parameters two objects, the base object and the one that will extend it. The properties of the extending object are iterated over and placed into the base object. This allows for a per-instance object extension mechanism. We’ll exploit this later when we implement the default values of the configurability parameters of the TextSuggest component.
The Prototype library also adds two methods to the Function object called bind() and bindAsEventListener(). These methods provide a syntactically elegant way to create function closures. You will recall other examples where we created closures, such as
oThis = this; this.onclick = function() { oThis.callSomeMethod() };
With the bind() method of Prototype, this can be expressed more simply as
this.onclick = this.callSomeMethod.bind(this);
The bindAsEventHandler() API passes the Event object to the method and normalizes the differences between IE and the W3C standard event model to boot!
A little-known fact about JavaScript is that you can name methods with certain special characters, such as the dollar sign ($). The Prototype library did just that to encapsulate one of the most common tasks in DHTML programming, namely, getting an element out of the document based on its ID. So, in our code we will be able to write constructs such as
$('textField').value = aNewValue;
rather than
var textField = document.getElementById('textField') textField.value = aNewValue;
We got Prototype for free by virtue of using Rico. Let’s talk about what we’ll be using from Rico. Rico has a rich set of behaviors, drag-and-drop capability, and cinematic effects, but since we are writing a component that uses a single text field, we won’t need most of these. What we will be able to use, however, is a nice Ajax handler and some of the utility methods provided by Rico. We will discuss the utility methods of Rico as the example progresses, but first let’s take a moment to discuss the Rico Ajax infrastructure.
The Rico Ajax capabilities are published via a singleton object available to the document named ajaxEngine. The ajaxEngine API provides support for registering logical names for requests as well as for registering objects that know how to process Ajax responses. For example, consider the following:
ajaxEngine.registerRequest( 'getInvoiceData', 'someLongGnarlyUrl.do' ); ajaxEngine.registerAjaxObject( 'xyz', someObject );
The first line registers a logical name for a potentially ugly Ajax URL. This logical name can then be used when sending requests rather than having to keep track of the aforementioned ugly URL. An example is shown here:
ajaxEngine.sendRequest('getInvoiceData', request parameters... );
The registerRequest() method isolates the usage of URLs to a single location, usually in the onload of the body element. If the URL needs to be changed, it can be changed at the point of registration without affecting the rest of the code.
Then registerAjaxObject() illustrates the registration of an Ajax handling object. The previous example implies that the object reference someObject should be referred to in responses by the logical name xyz and be set to handle Ajax responses via an ajaxUpdate() method.
Given that these functionalities of the ajaxEngine object are used, the only thing left to consider is the XML response expected by the Ajax engine. This is somewhat different from the dynamically generated JavaScript returned by the previous version of this example, but Rico expects to see XML. The response should have a top-level element around all of the <response> elements named <ajaxresponse>. Within that element, the server can return as many <response> elements as required by the application. This is a nice feature, as it allows the server to return responses handled by different objects that update potentially unrelated portions of a web page—for example, to update a status area, a data area, and a summary area. The XML response for the previous example is shown here:
<ajax-response> <response type="object" id="xyz"> ... the rest of the XML response as normal ... </response> <response...> more response elements if needed.. </response> </ajax-response>
This XML indicates to the ajaxEngine that an object registered under the identifier xyz should handle this response. The engine finds the object registered under the name xyz and passes the content of the appropriate <response> element to its ajaxUpdate() method.
Well, it was a short day overall. We spent some time researching open source frameworks to boost our productivity, and we came up with a game plan for incorporating them into our component. We’ve not yet written any code, but we have decided on a jump-start. We also have a good handle on a platform that will boost our performance, satisfying number 7 on our requirements list. Tomorrow we code.
Now that there’s a good technology platform to build on, let’s start writing the component. It’s often good to work backward from the desired result in order to think about the contract of our component up front. Let’s recap our first requirement:
This requirement forces us to leave pretty much everything inside the <body> alone. In light of that, let’s assume we’re going to inject our script into the HTML via code that looks similar to the HTML in listing 10.21.
The implication of this HTML is that we’re going to construct our object with the ID of the text field we will be attaching to, the URL for the Ajax data source, and a set of configuration objects yet to be specified. (Note that the text field needs an ID attribute for this to work properly.) Everything inside the <body> element is left untouched. With that established, let’s start with a look at the constructor. We’ll put a name for our TextSuggest component into the global namespace via the constructor function that, as you recall, is generated by the Prototype library’s Class.create() method, as shown in listing 10.22.
Now let’s deconstruct the constructor. As already mentioned, we pass into our constructor the ID of the text input to which we’ll be attaching the suggest behavior. A reference is held to both the ID and the DOM element for the input field . Next we do a little browser sniffing and store the state for the few things in the rest of the component that need to know specifics about the browser runtime environment . In this case, special case code is needed only for IE and Opera, so we sniff only for them.
We’ll discuss the complex part of setting up Ajax and injecting behavior later . Let’s concentrate for the rest of the day on component configurability. As you recall, earlier we created a SetProperties() function to hold all of the configurable aspects of our suggest script:
function SetProperties (xElem, xHidden, xserverCode, xignoreCase, xmatchAnywhere, xmatchTextBoxWidth, xshowNoMatchMessage, xnoMatchingDataMessage, xuseTimeout, xtheVisibleTime){ ... }
This meets the requirement of providing configurability but not of providing a convenient API or appropriate defaults. For this, we introduce an options object that is passed into the constructor. The options object has a property for each configuration parameter of the suggest component. Let’s now fill in the options with some configuration parameters:
var suggestOptions = { matchAnywhere : true, ignoreCase : true }; function injectSuggestBehavior() { suggest = new TextSuggest( 'field1', 'typeAheadXML.aspx', suggestOptions ); } );
This simple idiom comes with a big-time payload:
The last bullet is exactly what the setOptions() method shown earlier in the constructor does. Let’s look at how it works:
setOptions: function(options) { this.options = { suggestDivClassName: 'suggestDiv', suggestionClassName: 'suggestion', matchClassName : 'match', matchTextWidth : true, selectionColor : '#b1c09c', matchAnywhere : false, ignoreCase : false, count : 10 }.extend(options || {}); },
Each property in the options object that has an appropriate default value is specified here. Then, the extend() method of the Prototype library is called to override any properties specified in the options object passed in at construction time. The result is a merged options object that has the defaults and overrides specified in a single object! In the example we used here, the matchAnywhere and ignoreCase boolean properties were both overridden to values of true. The values of the configuration properties are explained in table 10.3.
Value |
Explanation |
---|---|
suggestDivClassName | Specifies the CSS class name of the div element that will be generated to hold the suggestions. |
suggestionClassName | Specifies the CSS class name of the span element that is generated for each suggestion. |
matchClassName | Specifies the CSS class name of the span holding the portion of the suggestion that matches what the user has typed in. |
matchTextWidth | A boolean value indicating whether or not the div generated for the suggestions should size itself to match the width of the text field it is attached to. |
selectionColor | Specifies a hex value (or any valid value used as a CSS color specification) for the background color of the selected suggestion. |
matchAnywhere | A boolean value that specifies whether the match should be looked for only at the beginning of a string or anywhere. |
ignoreCase | A boolean value indicating whether or not the matching should be case sensitive. |
count | The maximum number of suggestions to render. |
Note that there are several options that specify which CSS class names should be generated internally when building the HTML structure for the pop-up list of suggestions. Recall the configurability requirements from table 10.2:
Our code will use this configuration mechanism to provide per-instance configurability in terms of behavior (for example, case matching) as well as styling (for example, the CSS class names).
So, here at the end of day 2, we’ve made a good start on our component. With our constructor out of the way, and with a clean way to make our component highly configurable, it’s time to move on to making it Ajax aware.
Let’s put some Ajax into action, shall we? A TextSuggest component without Ajax is like a burger without the beef. With no disrespect to vegetarians, it’s time for the beef. You already saw a hint of some Ajax setup when we were looking at the constructor. As you might recall, we placed a method call within the constructor called initAjax(). The initAjax() method does the setup required for the Rico Ajax support discussed earlier. Here’s the implementation:
initAjax: function(url) { ajaxEngine.registerRequest( this.id + '_request', url ); ajaxEngine.registerAjaxObject( this.id + '_updater', this ); },
Recall that the registerRequest() method provides the Rico Ajax engine a logical name for the URLs to invoke for a given request via the sendRequest() method. Given that we have to support the requirement of having multiple suggest components on the same page using different URLs (but using the same ajaxEngine singleton), we need to generate a unique logical name for each. So, we generate the name for the request based on the ID of the component, which we assume to be unique. The same goes for the handler registration. We register this as the object that will handle responses routed to the ID we’re generating.
An example would probably help at this point. Suppose we attach the suggest behavior to a field with id='field1', and then we effectively register ourselves as 'field1_updater'. The XML we expect to come back to this component should have a response element that looks like this:
<ajax-response> <response type='object' id='field1_updater'>. ...same xml content as before. </response> </ajax-response>
Internally, we will be sending requests via the following:
ajaxEngine.sendRequest( 'field1_request', 'param1=val1', 'param2=val2', ... );
With that in mind, there are two things we have to do from the client to make our component Ajax enabled: send the request and handle the response. Let’s look at each in turn.
Obviously there’s a little bit of work involved in getting to the point where we can send a request. The text input will have to generate an onchange event that we will listen to and conditionally send a request for the suggestions. We’ve not put any of that code in place yet, but that’s okay. We can still think in terms of our method responsibilities and the contracts we’d like to enforce independently of that being done. So, let’s assume that some piece of code yet to be written will decide that it needs to send a request to get some suggestions. Let’s call it sendRequestForSuggestions() and implement it as follows:
sendRequestForSuggestions: function() { if ( this.handlingRequest ) { this.pendingRequest = true; return; } this.handlingRequest = true; this.callRicoAjaxEngine(); },
All this code does is to conditionally call this.callRicoAjaxEngine() if a request is not still being processed. This simple mechanism turns an internal boolean property, this.handlingRequest, to true as soon as an Ajax request is made and back to false (shown later) once the request has been handled. This is a very simple mechanism to use to throttle the sending of events based on the speed of the server. The boolean property this.pendingRequest is set to true if the method is called while a request is currently being processed. This state will let the handler know that it may have to send another request once the one being processed is finished. Now let’s peek under the hood and look at the callRicoAjaxEngine() method shown in listing 10.23.
To understand what this method does, we first need to talk about a JavaScript mechanism we are making use of on the very last line of the method:
ajaxEngine.sendRequest.apply( ajaxEngine, callParms );
This uses a method called apply(), which is available to all function objects (see appendix B for more details). Let’s illustrate the usage with a simpler example:
Greeter.prototype.greetPeople = function(str1, str2) { alert('hello ' + str1 + 'and ' + str2) };
Suppose we have an instance of Greeter called friendlyPerson, and we want to call the greetPeople() method on that object. But we don’t have the parameters in a form that is easy to pass. We actually have an array of people. This is where the apply method comes in handy. We can write the code as
var people = [ "Joe", "Sally" ]; friendlyPerson.greetPeople.apply( friendlyPerson, people );
The apply() method converts the array passed in as the second argument to first-class method parameters and invokes the method on the object passed in as the first parameter. The previous code is equivalent to
friendlyPerson.greetPeople( people[0], people[1] );
Now back to the task at hand. We have to call ajaxEngine’s sendRequest() method, which takes as its first parameter the logical name of the request, and a variable number of string parameters of the form key=value representing the request parameters. Therein lies the rub. We have request parameters from different sources, and we don’t know how many we have. Let’s look at the code again:
The array of parameters to send to the sendRequest() method via apply is populated from a combination of the internal state of the object, things like the ID and the lastRequestString, as well as specific properties of the Options object (for example, count, matchAnywhere, ignoreCase) .
However, we also have to provide a mechanism for the user of our component to pass in external request parameters as well . For this, we look for the existence of a requestParameters property on the options object. If it is non-null, it’s assumed to be an array of strings of the form key=value. The array is iterated over and added to the callParms already populated with the component-specific parameters. Finally, the request is sent via
ajaxEngine.sendRequest.apply( ajaxEngine, callParms );
Whew! Request sending all done. Now let’s hope the server is up and running and we get a response. And let’s talk about how we will handle it when it does.
We went to a lot of trouble to provide a robust request-sending capability, so we’d better make sure we properly handle the response or all our hard work will be in vain. Recall that Rico’s ajaxEngine routes the request back to the handler object’s ajaxUpdate() method, passing the content of the <response> element. So, by implication, we must write an ajaxUpdate() method, and that method will be the entry point into our response handling. The ajaxUpdate() method is shown in listing 10.24 along with its parsing helper methods, createSuggestions() and getElementContent().
Because we want to focus solely on the Ajax mechanisms being put in place, we’ll just cover much of the content here at a high level and talk about our response handling in terms of the algorithm. The first thing we do is to parse the response via the createSuggestions() method into an in-memory representation of the suggestions held in the suggestions property. The suggestions property is an array of objects, each with a text and a value property corresponding to the <text> and <value> elements of each <entry> in the XML response.
The remainder of the ajaxUpdate() method’s algorithm is fairly straightforward and should be easy to follow. If no suggestions were found, the pop-up is hidden and the internal value held by the component via a hidden field is cleared. If suggestions were found, the drop-down UI element is created, populated with the suggestions, and displayed, and the selection is updated to be the first one in the list. At this point, the response is considered to be handled, so the this.handlingRequest property discussed earlier is set back to false. Finally, the ajaxUpdate() method checks if there are any pending requests. If so, it sets the pendingRequest flag back to false, takes the current value in the input field for the lastRequestString, and initiates another request cycle via sendRequestForSuggestions().
This concludes the full request/response cycle for the Ajax support and wraps up day 3. We’ve accomplished quite a bit today, plugging in an open source framework that fully “Ajax-enables” our component-meeting requirement, number 7, as well as making sure that it’s done in a way that’s configurable and supports multiple instances on the same page, satisfying requirements 2 and 3. We’ll get into the details of what it means to create, position, show, and hide the UI for the pop-up on day 5. In the meantime, we’ll hook up the component events and take care of the keyboard and mouse handling.
Now that the suggest component is fully Ajax enabled, it’s time to hook it into the events produced by the native input field’s responses to the keyboard. If you are an astute reader, you will have guessed that the code that initiates this process was back in the constructor hiding in a call to the injectSuggestBehavior() method. This is the code that initiates all modifications to the DOM of the existing markup, including the event handling, extra inputs, and the container for the suggestions. It’s all done programmatically so we don’t have to touch any of the HTML code on the page, per requirement number 1. The behavior injection is shown in listing 10.25.
This method first checks to see if the browser is IE and, if so, sets the proprietary autocomplete property value to off. This keeps the autocompletion pop-up from interfering with our own pop-up. Next an object called TextSuggestKeyHandler is created to be the controller object for brokering the events to the right methods. Yes, the event mechanics are enough of a chore on their own that we split this behavior out into a separate object that we will discuss in a moment. The method next inserts a couple of input elements into the markup. You will recall that in the previous round of our script code, we added a hidden input field for storing the value of the component and an invisible text field to prevent the Enter key from causing the form to be submitted. Because our first requirement forbids us from monkeying with the HTML, we programmatically perform these chores with the two Insertion.After() calls. Insertion.After() is brought to us courtesy of the Prototype library. Finally, createSuggestionsDiv() is called to create the containing div element, which holds the UI for the suggestions.
We’ve decided to put the broker of the events into a dedicated controller class. There’s nothing new or revolutionary about it, but it’s definitely a helpful way to separate class responsibilities. In reality, the design could be further separated by creating explicit classes for the model and view roles to provide a full MVC pattern. This exercise is left to the user, but we will break down the architecture of the RSS reader in chapter 13 with a set of classes that satisfies a traditional MVC pattern.
The controller is constructed in the same way as our main class—using Class.create() and an initialize() method. The constructor is shown in listing 10.26.
Upon construction, the controller holds a reference to the suggest component along with the native HTML form input field. It then adds the handlers onto the input field via this.addKeyHandling(). The addKeyHandling() method is shown in listing 10.27.
addKeyHandling: function() { this.input.onkeyup = this.keyupHandler.bindAsEventListener(this); this.input.onkeydown = this.keydownHandler.bindAsEventListener(this); this.input.onblur = this.onblurHandler.bindAsEventListener(this); if ( this.isOpera ) this.input.onkeypress = this.keyupHandler.bindAsEventListener(this); },
All the relevant events that we need to listen to along with the Opera-specific hack mentioned in the first round of our script development are set up in this method. You will recall that the bindAsEventListener() method is a closure mechanism provided courtesy of the Prototype library. This mechanism allows our handlers to call first-class methods on the controller and normalizes the IE and W3C event models. Very nice, indeed. keyupHandler(), keydownHandler(), onblurHandler(), and their helper methods are mostly a repackaging of what’s already been covered with a few changes. We’ll show the full range of methods next and point out differences from the original script along the way. We’ll start by discussing keydownHandler() and its manipulation of the selection. The keydownHandler() method is shown in listing 10.28.
keydownHandler: function(e) { var upArrow = 38; var downArrow = 40; if ( e.keyCode == upArrow ) { this.textSuggest.moveSelectionUp(); setTimeout( this.moveCaretToEnd.bind(this), 1 ); } else if ( e.keyCode == downArrow ) { this.textSuggest.moveSelectionDown(); } },
The most significant difference from the original script in terms of functionality is in the handling of the arrow keys. The arrow keys in our TextSuggest component handle the movement of the selection based on the onkeydown event rather than the onkeyup event. This is done solely as a usability improvement. It’s somewhat disconcerting to see the selection remain where it is when you press one of the arrow keys, only to see it move once you release the key. keydownHandler() therefore handles the movement of the selection. Note that the selection manipulation methods are methods of the TextSuggest component. The controller, because it saved a reference to the component at construction time, can call these methods through the saved object reference this.textSuggest. The selection manipulation methods of TextSuggest are shown in listing 10.29 for the sake of completeness.
The updateSelection() method does all the real work of actually changing the visual state of the selection. It updates the span created in the selection list—we’ll write that code on day 5—and sets its style.backgroundColor to the value specified as the options.selectionColor of our component’s Configuration object.
Before we leave the topic of key-down handling, there’s one more bit of bookkeeping to take care of. Because we handle the arrow keys on the key-down rather than the key-up, we have to change the Up Arrow from its default behavior of moving the caret backward within the text field. We do this with the moveCaretToEnd() method called on a one-millisecond delay via setTimeout. The moveCaretToEnd() method is implemented as shown in listing 10.30.
moveCaretToEnd: function() { var pos = this.input.value.length; if (this.input.setSelectionRange) { this.input.setSelectionRange(pos,pos); } else if(this.input.createTextRange){ var m = this.input.createTextRange(); m.moveStart('character',pos); m.collapse(); m.select(); } },
Now, let’s move onto the key-up handling. The key-up implementation is a bit simpler than the key-down. All it has to do is broker its event to one of a couple of places based on the value in the input field and the key that was pressed. Let’s take a look at the details in listing 10.31.
keyupHandler: function(e) { if ( this.input.length == 0 && !this.isOpera ) this.textSuggest.hideSuggestions(); if ( !this.handledSpecialKeys(e) ) this.textSuggest.handleTextInput(); }, handledSpecialKeys: function(e) { var enterKey = 13; var upArrow = 38; var downArrow = 40; if ( e.keyCode == upArrow || e.keyCode == downArrow ) { return true; } else if ( e.keyCode == enterKey ) { this.textSuggest.setInputFromSelection(); return true; } return false; },
The key-up handler first checks to see if the input field contains any text. If not, it tells the TextSuggest component to hide its pop-up list of suggestions. Next it checks to see if the key pressed was one of the special keys: Up Arrow, Down Arrow, or the Enter key. If either the Up or Down Arrow key was pressed, the method just returns without performing any action, since the arrow keys have already been handled during the key-down processing. However, if the Enter key was pressed, the method tells the TextSuggest component to set its input value based on the currently selected item in the suggestion list. Finally, if the input field has a value and the key pressed was not one of the special keys, the key-up handler tells the TextSuggest component to consider that there is some input to be processed via the textSuggest.handleTextInput() method. This is the method of the TextSuggest component that finally calls the Ajax infrastructure we diligently put in place yesterday. The code for handleTextInput() is implemented in listing 10.32.
The handleTextInput() method first sets a local variable called previousRequest to the prior value of this.lastRequestString. It then sets the lastRequestString property to the current value of the input field so that it can compare the two to make sure that it’s not trying to send a request for the same information that has already been requested. If the request is an empty string, the pop-up list is hidden. If the request is a valid request for new information, the handleTextInput() method calls the sendRequestForSuggestions() method that we wrote yesterday to call the Ajax-based data source to get some suggestions from the server. If the request is the same as the last one, the request is ignored and no action is taken. Finally, the pieces are starting to come together. The construction, the configuration, the Ajax handling, the event handling—it’s almost as if we know what we’re doing. And just in the nick of time; it’s already day 4!
We have one more method of our controller class to cover—the onblur handler. The onblur handler is a very simple method that sets the value of the text field from the current selection and hides the suggestion. The implementation is as follows:
onblurHandler: function(e) { if ( this.textSuggest.suggestionsDiv.style.display == '' ) this.textSuggest.setInputFromSelection(); this.textSuggest.hideSuggestions(); }
The onblurHandler and handledSpecialKeys both reference a method of the TextSuggest component that we’ve not seen yet—setInputFromSelection(). This method does essentially the same thing that our SetText() function did earlier—namely, to take the currently selected suggestion; set both the input field and the hidden field with its text and value, respectively; and hide the list of suggestions. The implementation is shown here:
We may have put in a little overtime to accomplish all that’s been done today. We created a controller class to handle all of our event management. We used the Prototype library’s bindAsEventListener() method to automatically create closures for us and normalize the IE and W3C event models. We implemented our key-up/down handlers to encapsulate the complexities of processing the selection as well as normal text input. We ensured that we initiate only requests for new information. We managed the showing and hiding of the suggestions UI as appropriate. We updated the DOM programmatically to manage the hidden input value and the invisible text field that prevents form submission when the Enter key is pressed. And we handled the updating of the hidden and visible values of the TextSuggest component. On day 5, we wrap a bow around our refactored component by implementing all the methods required to create the pop-up, position it, show it, hide it, and manage its mouse events. The once dim light at the end of the tunnel is now clearly in view.
Now that we’re fully plugged in, so to speak, it’s time to tie up all the loose ends. To this point, we’ve created infrastructure for configurability and defaults, Ajax request and response handling, and the events that tie everything together. All that’s left to cover is the graphical part. What we’re referring to here, obviously, is the pop-up list of suggestions and all that implies. The tasks left to handle with respect to the UI are as follows:
Let’s go back and examine the implementation of the injectSuggestBehavior() method. Recall that this code was more or less the entry point to all the DOM manipulation done by the TextSuggest component:
injectSuggestBehavior: function() { // HTML Dom Behavior Injection... this.createSuggestionsDiv(); },
The last line of the injectSuggestBehavior() method calls the createSuggestionsDiv() method, which creates the outermost containing div of the suggestion pop-up. Since this is the container of all GUI artifacts, it’s the logical place to start looking at UI code. The details of the implementation are shown in listing 10.33.
The creation method of the container has four basic responsibilities. First, it has to create the DIV via the document’s createElement() API .
Second, it has to style the DIV according to the client configuration . Recall that one of our requirements was to make the CSS styling of each component instance individually configurable. We achieve that in this case by setting the div’s className attribute according to the suggestDivClassName property of the options object. You will recall that we set the default value of this property to suggestDiv within the setOptions method. So if the user doesn’t explicitly specify a value for a property, this is what she will get. This is a convenient feature because it allows the client of our component to have a default stylesheet that uses our default class names to style all TextSuggest component instances used across the application. Other stylesheets could also be provided (for example, product- or customer-specific stylesheets) that override the definitions of these standard style names. And finally, an individual page can override the value of the suggestDivClassName parameter to provide a page-level or instance-level styling to the component. Sounds pretty flexible to us.
There are certain aspects of the style of the pop-up that are nonnegotiable, annotated as “Behavioral style,” so we style them explicitly through the style attribute of the element . Note that anything styled programmatically via the style attribute overrides anything specified via a CSS className, typically by a stylesheet. These nonnegotiable aspects are 1) position='absolute' because the component must manage the positioning of the div internally, 2) zIndex=101, which we use to make sure the pop-up is on top of everything on the page, and 3) display="none" because the pop-up has to be hidden from the user’s view until the user’s keystrokes trigger it. Note that the value of 101 for the zIndex is somewhat arbitrary.
Finally, the method inserts the div into the document as a sibling of the text field . The parent in this case really doesn’t matter, since the div will be positioned absolutely.
Now that our pop-up has been created, at some point it will have to be shown. But before it can be shown, it has to be positioned. When we show the pop-up, we want it to appear just below the text field and to be aligned with the left side of the text field. Let’s write the positionSuggestionsDiv method in listing 10.34.
positionSuggestionsDiv: function() { var textPos = RicoUtil.toDocumentPosition(this.textInput); var divStyle = this.suggestionsDiv.style; divStyle.top = (textPos.y + this.textInput.offsetHeight) + "px"; divStyle.left = textPos.x + "px"; if ( this.options.matchTextWidth ) divStyle.width = (this.textInput.offsetWidth – this.padding()) + "px"; },
You will recall that in the previous version of this script, we wrote a method to calculate the absolute position of the text field. In this refactored version, we are relying on a utility method provided by Rico—toDocumentPosition(). All we have to do is to use this method to get our reference point and perform the appropriate calculations to get our pop-up below and align on the left with the text field. We then check for the existence of the configuration option matchTextWidth, and if it is true, we also size the width of the div element to match the width of the text input. Note that we adjust the width by the padding value. We do this because, as you recall, we’ve allowed the div element to be externally styled through a CSS class. We don’t know if the user will have put margins and borders on the component, which would throw off the visual alignment to the width of the text field. Let’s write a padding() method (listing 10.35) to compute the left and right padding values and margins to subtract from the overall width.
padding: function() { try{ var styleFunc = RicoUtil.getElementsComputedStyle; var lPad = styleFunc( this.suggestionsDiv, "paddingLeft", "padding-left" ); var rPad = styleFunc( this.suggestionsDiv, "paddingRight", "padding-right" ); var lBorder = styleFunc( this.suggestionsDiv, "borderLeftWidth", "border-left-width" ); var rBorder = styleFunc( this.suggestionsDiv, "borderRightWidth", "border-right-width" ); lPad = isNaN(lPad) ? 0 : lPad; rPad = isNaN(rPad) ? 0 : rPad; lBorder = isNaN(lBorder) ? 0 : lBorder; rBorder = isNaN(rBorder) ? 0 : rBorder; return parseInt(lPad) + parseInt(rPad) + parseInt(lBorder) + parseInt(rBorder); }catch (e){ return 0; } },
Getting the calculated style of an element—the actual value of an attribute regardless of how it was set—is tricky business. To achieve this, IE provides a proprietary currentStyle attribute for each element. Mozilla-based browsers use a getComputedStyle() method of the defaultView property of the document to calculate this. Each one of these mechanisms expects a different specification for the attribute being queried, as well. The IE currentStyle expects style attributes specified via the JavaScript-like binding (for example, borderRightWidth), whereas the Mozilla getComputedStyle() expects attributes specified with the stylesheetlike syntax (for example, border-right-width). Luckily, Rico provides a method that takes care of all of this for us—RicoUtil.getElementsComputedStyle(). We just pass it the element, the IE name for the attribute, and the Mozilla name for the attribute, and the method returns a value. Our method here gets the values of the left and right borders and margins, sums them up, and returns them.
The Rico.getElementsComputedStyle() is known to have issues with some versions of Safari, and so we provide a default return value within a try...catch block.
Now that we have the code to create and position the pop-up, we need to write a method to populate it with actual suggestions before it can be useful. Recall that our ajaxUpdate() method parses the XML from the response into an array of suggestion objects. And, if at least one suggestion exists, it calls a method named this.updateSugggestionsDiv(). This method is the transformer of the in-memory collection of suggestions to actual SPAN elements within the pop-up div. Let’s look at how that’s done now:
This method is deceptively simple, but there’s still lots of work to do, so hang with us. This method simply sets the value of the innerHTML property of the suggestionsDiv created earlier to an empty string in order to wipe out any prior content. Then it calls createSuggestionSpans() to create a span for each suggestion in the suggestions array. Finally, it iterates over the created spans and appends them to the div. This is where the real work starts. Let’s continue by looking at createSuggestionSpans() in listing 10.36 to see what’s involved in creating them.
createSuggestionSpans: function() { var regExpFlags = ""; if ( this.options.ignoreCase ) regExpFlags = 'i'; var startRegExp = "^"; if ( this.options.matchAnywhere ) startRegExp = ''; var regExp = new RegExp( startRegExp + this.lastRequestString, regExpFlags ); var suggestionSpans = []; for ( var i = 0 ; i < this.suggestions.length ; i++ ) suggestionSpans.push( this.createSuggestionSpan( i, regExp ) ); return suggestionSpans; },
This method first looks at our options object to find the value of the ignoreCase and matchAnywhere properties. This has to be done so that a regular expression can be created with the appropriate parameters that will facilitate the retrieval of the portion of the string in the response that actually matches what the user has typed in. The method then iterates over the suggestions property, which you will recall is an array of objects that have a .text and a .value property. For each suggestion in the array, the createSuggestionSpan() method is called with the index of the suggestion and the regular expression created earlier. All the real work is done in createSuggestionSpan(), shown in listing 10.37.
createSuggestionSpan: function( n, regExp ) { var suggestion = this.suggestions[n]; var suggestionSpan = document.createElement("span"); suggestionSpan.className = this.options.suggestionClassName; suggestionSpan.style.width = '100%'; suggestionSpan.style.display = 'block'; suggestionSpan.id = this.id + "_" + n; suggestionSpan.onmouseover = this.mouseoverHandler.bindAsEventListener(this); suggestionSpan.onclick = this.itemClickHandler.bindAsEventListener(this); var textValues = this.splitTextValues( suggestion.text, this.lastRequestString.length, regExp ); var textMatchSpan = document.createElement("span"); textMatchSpan.id = this.id + "_match_" + n; textMatchSpan.className = this.options.matchClassName; textMatchSpan.onmouseover = this.mouseoverHandler.bindAsEventListener(this); textMatchSpan.onclick = this.itemClickHandler.bindAsEventListener(this); textMatchSpan.appendChild( document.createTextNode(textValues.mid) ); suggestionSpan.appendChild( document.createTextNode(textValues.start) ); suggestionSpan.appendChild(textMatchSpan); suggestionSpan.appendChild( document.createTextNode(textValues.end) ); return suggestionSpan; },
This task is starting to look daunting, but don’t bail out just yet. This method probably looks more complicated than it is, although it does quite a bit of work. Perhaps it would be best to back up at this point and look at this method in terms of what it is attempting to produce: namely, some HTML for a suggestion. Let’s imagine HTML markup for a suggestion that looks something like this:
<span>before <span>matching text</span>, and after</span>
This is a gross simplification of what’s actually generated, but it illustrates the structure. Suppose that the user has typed “matching text,” and one of the values in the database is “before matching text, and after.” What’s generated for a suggestion is basically what we just showed but with some extra attributes added to the spans for identification, styling, and event handling. All the hard work of splitting up the before and after portions of the text is done by the following line of code:
var textValues = this.splitTextValues( suggestion.text, this.lastRequestString.length, regExp );
The textValues value returned is an object that has three properties: .start, .mid, and .end. So in the example just shown, textValues is an object that looks like the following:
textValues = { start: 'before ', mid: 'matching text', end: ', and after' };
Finally, the splitTextValues() method implementation is shown here:
splitTextValues: function( text, len, regExp ) { var startPos = text.search(regExp); var matchText = text.substring( startPos, startPos + len ); var startText = startPos == 0 ? "" : text.substring(0, startPos); var endText = text.substring( startPos + len ); return { start: startText, mid: matchText, end: endText }; },
Now that we’ve covered the basic structure of a suggestion span, let’s talk about the relevant attributes that get generated on the spans. Both the outer span and the inner span are created with CSS class names based on the value of the suggestionClassName and matchClassName properties of the Options object, respectively. Just as the suggestionsDiv has an entirely customizable look and feel via CSS classes, so does all of the internal HTML structure of each suggestion.
The other noteworthy attributes generated within the spans are ID attributes so that the spans can be retrieved later by the aforementioned event handlers. An onmouseover event handler has to be placed on the spans so that the component can update the selection to the suggestion that the mouse is currently over. Also, an onclick event handler must be placed on each suggestion so that when a suggestion line is clicked on, its value can be placed within the text field. The two event handlers are implemented as shown in listing 10.38.
mouseoverHandler: function(e) { var src = e.srcElement ? e.srcElement : e.target; var index = parseInt(src.id.substring(src.id.lastIndexOf('_')+1)); this.updateSelection(index); }, itemClickHandler: function(e) { this.mouseoverHandler(e); this.hideSuggestions(); this.textInput.focus(); },
mouseoverHandler() simply finds the target of the event and parses out the ID that we generated on it to get an index representing which suggestion it is. It can then use the updateSelection() method we wrote on day 4 to update the selection to the suggestion over which the mouse is currently hovering.
Similarly, itemClickHandler() has to update the selection, so it just calls mouseoverHandler() to do the selection update work. It then has to do the additional behavior of hiding the suggestions pop-up via a call to the hideSuggestions() method and giving the focus back to the text field so the user can continue typing.
We’ve finally completed the pop-up creation task. Now let’s concentrate on the infinitely simpler task of hiding and showing it.
Now that we’ve developed code to handle all of the complex details of creating a pop-up list of suggestions, we need to write the code that shows and hides it. Fortunately, this is an extremely straightforward process, as any seasoned developer of DHTML like yourself knows. The showing and hiding of an element are typically done by manipulating the display property of an element’s style. This component will be no different. So without further ado, listing 10.39 contains the code that shows the pop up and the code that hides the pop-up.
The show and hide, as shown here, simply manipulate the style.display property of suggestionsDiv in order to show it (via an empty string value) and hide it (via none). The showSuggestions() method does the additional work of positioning the pop-up before showing it. That’s it! We mean that’s really it. Our component is done. Let’s take a few seconds to debrief.
This was certainly a fairly complex component with a lot of moving parts. Grand architects or not, we’ve developed a reusable component to be proud of. Our TextSuggest component handles a wide range of configuration parameters, it’s extensible, it’s server-agnostic, it’s unobtrusive, it’s cross-browser, it has a simple API for creation, it slices, it dices... Well, maybe it’s not all that, but seriously, it’s pretty cool, and it covers all the bases that we listed in table 10.2. The component source code is available in its entirety at http://www.manning.com/crane. Rico can be found at http://openrico.org/ and Prototype at http://prototype.conio.net/.
The type-ahead suggest lets your users save time by offering the options that they may need as they are typing. When they type a few keystrokes on the keyboard, the data that they want is available for selection. This chapter has looked at the downfalls of the currently available implementations and has initiated an application that lets us eliminate any unnecessary postbacks to the server by doing most of the processing on the client. We have worked with DHTML to create a dynamic user interface that allows interaction with the keyboard and the mouse. This example shows how Ajax can be used effectively to add a seamless interaction with the server without disrupting the user’s interactions with the web page. This script also degrades well with browsers that do not support Ajax, since the type-ahead textbox acts like a plain textbox into which users can enter data rather than just having a quick solution at their fingertips. Finally, we pushed the envelope of object-oriented JavaScript component development by refactoring the script into an easily created, configured, and used TextSuggest component.
3.145.178.159