How to Create an Ajax Autocomplete Text Field - Part 9 | WebReference

How to Create an Ajax Autocomplete Text Field - Part 9

By Rob Gravelle


Digg This Add to

In "Part 7: Moving to a CSS Design", we updated the control's layout to CSS and decoupled it from the Autocomplete.jsp page in order to make it into a standalone Web control. Then, in Part 8, we added a scrollbar to the list using a <jsp:param> tag to pass the list size in rows. This week we continue to add functionality and tweak the appearance of our control by implementing search string matching in the list items and by adding a CSS drop shadow effect. Next time, we'll be adding support for the arrow and control keys.

The highlightClass Parameter

Like the scrollbarColorClass parameter before it, the highlightClass allows the user to specify a CSS class that will control the appearance for search string match highlighting. The highlighting feature can easily be turned off by supplying an empty string for the parameter value. Here's the bottom part of the Autocomplete control <jsp:include> tag contents, on the AutocompleteSearchCSS.jsp page:

On the AutocompleteControl.jsp page, a variable by the same name (highlightClass), will store the parameter for later use:

Tracking the searchString

The searchString is used in the autoComplete() function where it's submitted via an Ajax call to the server. The search string highlighting happens later, after the Ajax onreadystatechange event fires with a readyState of 4. Since this is done asynchronously, passing the string to the callback function is a risky proposition. Therefore, we should bind it to the event. In fact, it's always a good idea to bind any objects or variables that are required in the callback function because of the potential time lag. In a previous article, entitled Universally Related Popup Menus with JavaScript: Ajax Edition, I introduced the bind() function:

The bind() function ... is used to provide the [callback] function with the objects and variables that it needs when the XMLHttpRequest fires the onreadystatechange event. It does so by creating local variables within the function on which it's applied. The function that bind() is being applied to ... is saved in the method variable. The first argument is the object which will define the scope of the callback function ... All the other arguments will be passed along to the calling function, but in order to do that, we have to separate them from the first argument. The Array object has the perfect method called slice(). It splits the array from the element number that we provide and returns the remaining elements as a new Array. Unfortunately, it turns out that the arguments property is not a true Array. Although it shares some of the same properties such as length, it does not implement all the methods of a true Array object. There are two ways around this: we write our own slice() function, or, we steal the existing one from the Array object! You know what they say about reinventing the wheel. Reuse! Once we've copied the slice() function over to the arguments property, we can call it just as if it always existed. The return type is a function because the onreadystatechange property expects one:

Adding the bind() function to the Function object's prototype property allows us to call it from any function using the dot notation:

Hence, we can update our code in the autoComplete(Event) function to bind the searchString to our anonymous function. The bind() function will set the req variable as the "this" object and pass the searchString to the anonymous function to be used later. Even though it's absent from the function's arguments list in the signature, it will available to the function through a JavaScript scoping phenomenon known as closures. The way it works is that by returning a function instead of an actual value, the scripting engine keeps track of the variable rather than destroy it as would usually occur. The reason is simple: the garbage collection only cleans up variables that are no longer in use. Since the returned function hasn't executed yet, passed parameters remain in memory. The searchString can then be passed directly to the parseMessages() function because there's no discernible time lag in execution between the anonymous function and parseMessages():

The following code snippet demonstrates how we add the searchString argument to the parseMessage() function's signature and alter the loop to send the searchString to the appendFund() function, where it will finally be used. The JavaScript 1.3 call() function is applied to appendFund() to set the current jsonObject array element as the this object and send the searchString as the function argument. If you think that the call() function closely resembles bind(), you're right! The bind() function is based on call(), except that it returns the bound function, rather than the function results:

The appendFund() Function

The appendFund() function's one parameter was the JSON Fund object, which can be replaced with the searchString, since the fund is the this object. A new function called highlightSearchString() will handle the highlighting on the fund symbol and name:

The highlightSearchString() Function

The highlightSearchString() function is added to the String object much like bind() was added to the Function object, so all strings will share it. The function accepts three parameters: the table cell containing the text, the searchString that we are matching against, and the CSS highlightClass rule that governs the appearance of the highlighted text. The string that contains the searchString is the this object. As we saw in the code snippet above, the highlightSearchString() function is called every time we add the fund field to the list. Therefore, the highlight formatting may or may not be applied. That's why it checks for the highlightClass parameter. If there isn't one to apply, then the full searchString is returned for displaying.

To match the searchString, we'll use a regular expression. The RegExp constructor takes two parameters: the pattern and modifiers. The caret (^) symbol tells it to match from the start of the test string. The "i" indicates that we want the matching to be case insensitive. The String's match() function takes our regular expression pattern and applies it against the test string, which is contained in the this object and returns the matching portion. If a match is found, we can create a <span> element to enclose the highlighted text. The text is included as a child of "text node" type to the span, which is then appended to the cell. Finally, the RegExp.rightContext property is returned to the calling function. It contains the remaining text after the matched portion. This text will also be appended to the cell as a text node in the appendFund() function:

Here is a sample of a typical table row generated by the appendFund() and highlightSearchString() functions above: