Avoid huge dropdowns! This tutorial shows how to dynamically create a suggest list as long as the user fills a form field.
Like Google Suggest!
NOTE: You can download all the files for this tutorial by going to: http://www.easycfm.com/downloads/suggest.zip
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:
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.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:
– 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 = ' class="suggestList"';
    for (var i=0;i' + '' + arr[i] + '' + '';
   }
   output = output + '';
   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
Name :
Country :
e-mail :
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.
References
1. Dynamic HTML and XML: The XMLHttpRequest Object
2. Combining XMLHttpRequest and Rails to Produce More Efficient UIs
*Originally published at EasyCFM
I’m a senior engineer, working for Embraer – a brazilian aircraft industry. I work in the flight test division where I’m responsible for ColdFusion training and propagation.
At Embraer we’ve created our own Macromedia User Group, which I’m manager.
I’ve been using ColdFusion since 1997 and I’ve been working hard at Web Standards for the last two years.