Building a Suggest List with XMLHttpRequest

Introduction

SELECT controls are very handy helping HTML forms filling. However, when you have a large number of options, (e.g. 100), finding and selecting one item can be a hard thing to do. Besides, page loading time increases a lot.

On december 2004, Google introduced Google Suggest. In the search form field, as long as we type, 'pre-search' id made and results are shown in a selection box below. This box works like a SELECT control.

Well, such a solution can be very useful when we have a huge lists. Instead of using a SELECT control, let's show a list - suggest list - containing the 'pre-search' result items. This 'pre-search' uses the text typed till then as an argument.

In this tutorial, we'll create an INPUT control with this behavior. We'll use some JavaScript functions. The main concept behind our suggest list is the XMLHttpRequest object. With this object, it?s possible to perform HTTP requests (GET and POST) in the background.

Implementation

We'll work on user register form. User's country is one of required information. A countries list is a large list (over 200 countries). So, it becomes a natural candidate to be filled via suggest list.

First, let's take a look at our work files:

form.htm
Main HTML page. Contains the form itself.
style.css
Used by FORM.HTM. Style sheet rules that apply to FORM.HTM elements.
coutries.cfm
In order to keep simple this tutorial, we'll not work with external databases. So, we create a query object from countries list. This query belongs to SESSION scope. As long as user types, a new search is performed querying the countries list, giving suggest list.
request.js
Used by FORM.HTM. All JavaScript code needed to send requests.

form.htm

Most important parts of FORM.HTM are shown below.


...
<head>
    ...
    <link rel="stylesheet" type="text/css" href="style.css" />
    <script type="text/javascript" src="request.js"></script>
</head>

<body onload="document.getElementById('country').focus();">

<form id="form" action="">
    ...
    <div id="countrySuggest" class="suggest">
        <input id="country" type="text" name="country" 
            onkeyup="doSearch(this.value);" autocomplete="off" />
        <div id="countrySuggestItems"></div>
    </div>
...

In section HEAD, we load REQUEST.JS and STYLE.CSS files. Inside BODY tag, we just set focus on country control.

Inside the form, there's a DIV containing the country control and another DIV (countrySuggestItems) which will hold suggest list. Note that the onkeyup event points to doSearch() function. This function will request and display suggest list. We'll talk about this function later.

countries.cfm

Essentially, this file creates a SESSION scoped query - qCountries - so it persists among requests. Once created, this query will be queried as if it is a database table.

Further, this file contains the list assembling based on a query of query qSearch.


<cfapplication name="suggestList" sessionmanagement="Yes"> 

<cfif NOT isDefined("session.qCountries")>
    <cfset lCountries = "Afghanistan,Alaska ...">
    
    <cfset session.qCountries = queryNew("name")>
    <cfloop index="country" list="#lCountries#" delimiters=",">
        <cfset queryAddRow(session.qCountries)>
        <cfset querySetCell(session.qCountries,"name",country)>
    </cfloop>
</cfif>

<cfquery name="qSearch" dbtype="query" maxrows="10">
    SELECT  name
      FROM  session.qCountries
     WHERE  upper(name) LIKE '%#uCase(URL.search)#%'
  ORDER BY  name
</cfquery>

<cfsetting enablecfoutputonly="Yes">
<cfoutput>#valueList(qSearch.name)#</cfoutput>

A query is performed on session.qCountries query, using url.search as search criterium. Finally, a comma delimited list containing countries names is returned.

request.js

This is the key point in our tutorial. All functions are simple, but they require former JavaScript knowledge. We'll go into these functions one by one:

doSearch()

This is the main function. It creates the XMLHttpRequest object which will send requests to countries.cfm page.

The XMLHttpRequest object supports two modes for executing your request: synchronous and asynchronous. Both work well, but the nice thing about asynchronous mode is that it won?t block the user?s browser from performing other tasks. For example, other JavaScript events will be able to be handled, such as onMouseOver events for image rollovers, while the web server is being contacted and communicated with in the background. For these reasons, we?ll go through the small amount of extra effort to implement asynchronous requests instead of synchronous; you are, of course, free to make your own decision, though!

In order to make a request asynchronously, an event handler (or 'callback') function must be defined and passed to the object making the requst. This function will then be called any time the ?ready state? of the request object changes, with the new ready state value passed in as a parameter. The following table lists the possible states and their values:

Value Description
0 Uninitialized
1 Loading
2 Loaded
3 Interactive
4 Complete

You will most likely be concerned only with the Complete status (4), but the others come in handy if, for example, you are displaying a progress bar on screen for the user. Once a request reaches the Complete state, the result of the request will be available to you.

Now we have a comma delimited list. We'll format this llist as an HTML table and make DIV countrySuggestItems visible.


function doSearch(searchString) {
    var sugItems = document.getElementById('countrySuggestItems');
    if(searchString.length>0) {
        // creates XMLHttpRequest object
        if (window.XMLHttpRequest) {
            req = new XMLHttpRequest();
        }
        else if (window.ActiveXObject) {
            req = new ActiveXObject("Microsoft.XMLHTTP");
        }
        // request countries.cfm passing searchString as an URL parameter
        req.open("GET", "./countries.cfm?search=" + searchString, true);
        req.send(null);
        // gives the request object an event handler
        req.onreadystatechange = function() {
            if ((req.readyState == 4) && (req.status == 200)) {
                // creates an array from returned list
                var arr = req.responseText.split(',');
                if (arr.length) {
                    // formats array as an HTML table and shows the DIV
                    sugItems.innerHTML = htmlFormat(arr);
                    sugItems.style.display = 'block';
                }
                // No items found? Hides the DIV
                else sugItems.style.display = 'none';
                return;
            };
        }
    }
    // Empty searchString? Hides the DIV
    else sugItems.style.display = 'none';
    return;
}    

Some points to note:

  1. req.open("GET", "./countries.cfm?search=" + searchString, true) - The open method accepts three parameters:
    • Request method: either GET or POST
    • URL for the request
    • Asynchronous? true or false
  2. req.send(null) - send 'extra' data along with the request. You must do this step for both GET and POST requests. For a GET request, you should just pass null; for a POST, you should pass a properly url-encoded string of name=value pairs
  3. req.onreadystatechange = function() - give the request object an event handler as described above

htmlFormat()

We've got search results as a list and we've converted it to an array. We shall use now htmlFormat() function to build an HTML table from this array.


function htmlFormat(arr) {
    // formats arr as an HTML table
    var output = '<table> class="suggestList"';
    for (var i=0;i<arr.length;i++) {
        output = output + '<tr onmouseover="this.style.backgroundColor=0xeeeeee;" 
            onmouseout="this.style.backgroundColor=0xffffff;" 
            onclick="getData(this)">' + '<td>' + arr[i] + '</td>' + '</tr>';
    }
    output = output + '</table>';
    return output;
}

Keep in mind that:

  1. <tr onmouseover="this.style.backgroundColor=0xeeeeee;" - changes row background color when mouse pointer is over
  2. <tr ... onmouseout="this.style.backgroundColor=0xffffff;" - changes row background color back when mouse pointer is out
  3. <tr ... onclick="getData(this)" - calls getData() function when user clicks on a row. The point is: assign the list selected value to country control

getData()

When user selects a row in suggest list, this function is then called. Clicked TR is passed as a parameter. We find the TD (and its value) inside this TR. This value is then assigned to country control and suggest list is made invisible.


function getData(obj) {
    // finds all TDs inside obj
    var arrTD = obj.getElementsByTagName('TD');
    // assigns TD value to form field
    document.getElementById('country').value = arrTD[0].innerHTML;
    // hides the DIV
    document.getElementById('countrySuggestItems').style.display = 'none';
}

Testing

Now you have all files, let's test the form and the suggest list. To do this, browse FORM.HTM page.

You can test the form below, if you want:

User Information

As long as we type, list is automatically updated. When we move the mouse pointer over the items, we can see the effect of onmouseover and onmouseout events.

Clicking an item, its value is assigned to country control and the list becomes invisible.

Some Improvements

Query more than one column
Showing more than one column inside suggest list can be handy. For example, employees name and department. To do this, we have to change query results to a more complex list and modify htmlFormat() function accordingly.
Using ID instead of names and descriptions
When we use a SELECT control, we use OPTION tag value attribute to hold ID values. In this case, more changes inside htmlFormat() function are required, once IDs should not be visible. Further, form must have a hidden control to hold selected ID value.
Build a CF_SUGGEST Custom Tag
All work - DIVs definition, JavaScript functions and CSS - will be done by this Custom Tag.
List layout definition outside JavaScript functions
Developers will define different list layouts according to their needs. Basically, they should redefine htmlFormat() function.
About This Tutorial
Author: Claudio Dias
Skill Level: Intermediate 
 
 
 
Platforms Tested: CFMX
Total Views: 153,251
Submission Date: March 11, 2005
Last Update Date: June 05, 2009
All Tutorials By This Autor: 3
Discuss This Tutorial
  • I have the same problem as Fernando oliveira. When i search on the first time the list appear... but the second time on the same letter the list dont appear... For eg: type lo, select solomon islands..now without refreshing the page, type lo again, the list will not appear in IE. Any fix?

  • Hello, Thanks for this tutorial!, I was trying to add the status icon but could get it to work. I was wondering if you can please add the to this tutorial. Thank you in advance!

  • When i search on the first time the list appear... but the second time on the same letter the list dont appear... Is IE problem?(Firefox works). Tks, FĂȘ.

  • Thank you for your work. I was able to utilize your ideas on one of my projects. Does any one know what changes we would need to make in order to be able to use up/down keyboard keys and enter button inorder to make a selection in addition to using the mouse?

  • Alexei, The idea behind this tutorial, xmlHttpRequest, is NOT an original idea! I've only put together all steps to build a simple interface like Google Suggest. Thank you for your comment and link.

  • This reminds me of a commercialized version of the g mail and g oogle lookup: http://developer.ebusiness-apps.com/technologies/webdevelopment/codeandcomponents/ebawebcombov3/media/demos.htm

  • Thank you for comments and vote!

  • Wow, this is extremely good! I didn't know this was even possible until today! I voted for you! Goodluck.

Advertisement

Sponsored By...
Deep Tissue Massage and Swedish Massage Services just $39 for a 50 minute massage!