Download the EasyCFM.COM Browser Toolbar!
Building a Suggest List with XMLHttpRequest
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.
All ColdFusion Tutorials By Author: Claudio Dias
  • Nested Custom Tags
    This tutorial explains how to use nested custom tags in ColdFusion. It also presents one 'basic quiz' in CF MX where nested custom tags show an interesting behavior
    Author: Claudio Dias
    Views: 16,550
    Posted Date: Monday, June 23, 2003
  • Feeding a Query
    A simple approach on how to create a query object from RDF/RSS feed. Further, when news are in HTML format, how to get them using Regular Expressions.
    Author: Claudio Dias
    Views: 15,437
    Posted Date: Tuesday, May 25, 2004
  • Build a List, Get a Tree
    How to create an outlined tree from <ul> and <li> tags, CSS and JavaScript
    Author: Claudio Dias
    Views: 19,811
    Posted Date: Tuesday, November 9, 2004
  • Building a Suggest List with XMLHttpRequest
    Avoid huge dropdowns! This tutorial shows how to dinamically create a suggest list as long as the user fills a form field. Like Google Suggest!
    Author: Claudio Dias
    Views: 31,293
    Posted Date: Friday, March 11, 2005
  • Building an Editable Grid with AJAX and ColdFusion Components
    This tutorial shows how to change an HTML table into an editable grid using JavaScript, XML and ColdFusion Components. No more page reloads! Its easy! Its simple!
    Author: Claudio Dias
    Views: 29,767
    Posted Date: Friday, September 16, 2005