Creating an Autosuggest Textbox with JavaScript, Part 1

By Nicholas C. Zakas.

Over the past year, Google has branched out from its search engine into other types of Web applications. One that caused a great deal of excitement among Web developers is Google Suggest. The basic idea is very simple: as you type, Google suggests search terms that come up with results. The first suggestion is filled into the textbox as you type while a list of several suggestions appears in a dropdown list beneath the textbox. If you haven't tried it yet, check it out before reading on.

The idea behind Google Suggest is something that has been used in desktop applications for some time and has only recently made it onto the Web. Google Suggest wasn't the first implementation of such an interface (check out the Bitflux Blog LiveSearch), but it popularized the technique among developers to the point where people have dissected the source code and attempted to recreate the functionality.

In this series of articles, you will learn how to build an autosuggest control one step at a time. First, you'll learn how to implement type ahead functionality (filling in the textbox as you type). Then, you'll add the dropdown suggestion list to the control. Last, you'll learn how to get the suggestions from the server.

The Basics

In building the autosuggest textbox, you will JavaScript in a true object-oriented fashion. The main implementation consists of two types of objects: one to represent the autosuggest control and one that provides the suggestions (which I call a suggestion provider). The autosuggest control does the heavy lifting in the code by handling all of the user interaction with the textbox. The suggestion provider is called by the control when suggestions are needed for the string in the textbox. In this way, it's possible to implement autosuggest functionality for more than one data set just by specifying a different suggestion provider. You could have one suggestion provider for colors, one for state names, and so on.

Building the Autosuggest Control

The first step is to build out the autosuggest control itself. The control needs two pieces of data passed into it, namely the textbox and the suggestion provider. Here's the class definition for the autosuggest control:

function AutoSuggestControl(oTextbox, oProvider) {
    this.provider = oProvider;
    this.textbox = oTextbox;
}

Simple enough? The textbox and suggestion provider and passed into the constructor and stored in properties. Next come the methods for the control.

Text Selection

In order to implement the first feature, type ahead, you need to understand how it works. Type ahead textboxes look at what the user has typed and then fills in a suggestion, highlighting only the part that was added automatically. For example, the following image shows what would happen when a user types in "bl" and the suggestion is the word "black":

Note that only the "ack" is highlighted in the textbox. To select only part of the text in a textbox, you can't use the regular textbox select() method, which selects all of the text. In fact, there's no standard governing such behavior. Luckily, the two most popular browsers, Internet Explorer and Mozilla, each provide their own (proprietary) ways of accomplishing this.

In Internet Explorer, you need to use a text range. A text range is an invisible encapsulation of parts of a textbox, beginning on a single character and ending on a single character. Once you have a text range, you can select just the text contained within it. Textboxes in IE have a method called createTextRange() which is used to create a text range for the textbox. There are many methods that a text range has, but the only ones of importance for this article are moveStart() and moveEnd(), which determine the parts of the text to surround. Each of these methods accepts two arguments: a unit and a number. The unit can be "character,""word,""sentence" or "textedit" while the number indicates the number of units to move from the start or end of the text (should be a positive number for use in moveStart(), a negative number for use in moveEnd()). When the endpoints of the range are set, you can call its select() method to select just those characters. For example, to select just the first three characters in a textbox, you could do this:

var oRange = oTextbox.createTextRange();
oRange.moveStart("character", 0);
oRange.moveEnd("character", 3 - oTextbox.value.length);
oRange.select();
oTextbox.focus();

Note that to get the appropriate value for moveEnd(), you must subtract the length of the text in the textbox from the number of characters to select (3). The last step is to set focus to the textbox so that the selection is visible (text can only be selected when the textbox has focus). The process is a bit involved in IE, but pretty easy to script. Mozilla, on the other hand, is very straightforward.

Textboxes in Mozilla have a non-standard method called setSelectionRange(), which accepts two arguments: the character to start with and the number of characters to select. To select the first three characters in a textbox using Mozilla, you need only two lines of code:

oTextbox.setSelectionRange(0,3);
oTextbox.focus();

With two separate ways to select specific characters in a textbox, it would be helpful to have a method to encapsulate this functionality. Here it is:

AutoSuggestControl.prototype.selectRange = function (iStart, iLength) {
    if (this.textbox.createTextRange) {
        var oRange = this.textbox.createTextRange();
        oRange.moveStart("character", iStart);
        oRange.moveEnd("character", iLength - this.textbox.value.length);
        oRange.select();
    } else if (this.textbox.setSelectionRange) {
        this.textbox.setSelectionRange(iStart, iLength);
    }

    this.textbox.focus();
};

This method uses feature detection, the process of detecting certain methods of the textbox, to determine how to select the characters. The arguments are the first character to select and the number of characters to select. These values are then passed to the browser-specific methods of text selection.

Implementing Type Ahead

Now that you can select only specific parts of the textbox, it's time to implement the type ahead functionality. The method defined here accepts a suggestion and assumes that it is appropriate given the contents of the textbox. The steps involved are:

  1. Get the length of the text already in the textbox.
  2. Place the suggestion into the textbox.
  3. Select only the portion of the text that the user didn't type using the information from step 1.

Additionally, since type ahead can only be supported in Internet Explorer and Mozilla, you should check to make sure one of those browsers is being used. If the browser doesn't support text selection, only the first two steps will be executed; which would disturb the user's typing. The solution is to skip the type ahead feature if it's not supported. Once again, testing for the createTextRange() and setSelectionRange() methods of the textbox is the way to go:

AutoSuggestControl.prototype.typeAhead = function (sSuggestion) {
    if (this.textbox.createTextRange || this.textbox.setSelectionRange) {
        var iLen = this.textbox.value.length;
        this.textbox.value = sSuggestion;
        this.selectRange(iLen, sSuggestion.length);
    }
};

But where does the suggestion come from? This is where the autosuggest() method comes in.

The autosuggest() Method

Perhaps the most important method in the control is autosuggest(). This single method is responsible for receiving an array of suggestions for the textbox and then deciding what to do with them. Eventually, this method will be used to implement the full autosuggest functionality (including dropdown suggestions), but for now, it's used to implement type ahead only.

Since autosuggest() will be passed an array of suggestions, you have your pick as to which one to use for the type ahead value. It's recommended to always use the first value in the array to keep it simple. The problem is that there may not always be suggestions for a value, in which case an empty array will be passed. To provide for this, you must first check to see if there are any items in the array; if there are, call the typeAhead() method and pass in the first suggestion, like this:

AutoSuggestControl.prototype.autosuggest = function (aSuggestions) {

    if (aSuggestions.length > 0) {
        this.typeAhead(aSuggestions[0]);
    }
};

You may be wondering at this point where the suggestions will come from. It's the job of a suggestion provider to pass in the array of suggestions to this method. This will be discussed further when the sample suggestion provider is created later on in the article. But first, you need to know how to interact with the user as he/she is typing.

Handling Key Events

Of course, the autosuggest functionality has to be tied in to the textbox using events. There are three events that deal with keys: keydown, keypress, and keyup. The keydown event fires whenever the user presses a key on the keyboard but before any changes occur to the textbox. This obviously won't help with autosuggest because you need to know the full text of the textbox; using this event would mean being one keystroke behind. For the same reason, the keypress event can't be used. It is similar to keydown, but fires only when a character key is pressed. The keyup event, however, fires after changes have been made to the textbox, which is exactly when autosuggest should begin working.

Setting up an event handler for the textbox involves two steps: defining a function and assigning it as an event handler. The function is actually a method of the autosuggest control called handleKeyUp(). This method expects the event object to be passed in as an argument (how to accomplish this is discussed later) so that it can tell whether the key being pressed should enact the autosuggest functionality. Since keyup fires for all keys, not just character keys, you'll receive events when someone uses a cursor key, the tab key and any other key on the keyboard. To avoid interfering with how a textbox works, suggestions should only be made when a character key is pressed. This is where the event object's keyCode property enters the picture.

The keyCode property is supported by most modern browsers (including Internet Explorer on Windows and Macintosh, Mozilla, Opera, and Safari) and returns a numeric code representing the key that was pressed. Using this property, it's possible to set up behaviors for specific keys. Since the autosuggest functionality should happen only when character keys are pressed, many think that you need to specify all of the character key codes. It's actually best to take the opposite approach and specify all of the non-character key codes to be ignored. This approach is more efficient because there are more character keys than non-character keys.

So which keys need to be ignored? Here's the complete list and their key codes:

Key Code Key Code
Backspace 8 Print Screen 44
Tab 9 Delete 46
Enter 13 F1 112
Shift 16 F2 113
Ctrl 17 F3 114
Alt 18 F4 115
Pause/Break 19 F5 116
Caps Lock 20 F6 117
Esc 27 F7 118
Page Up 33 F8 119
Page Down 34 F9 120
End 35 F10 121
Home 36 F11 122
Left Arrow 37 F12 123
Up Arrow 38    
Right Arrow 39    
Down Arrow 40    

You may notice a pattern among the key codes. It looks like all keys with a code less than or equal to 46 should be ignored and all keys with codes between 112 and 123 should be ignored. This is generally true, but there is an exception. The space bar has a key code of 32, so you actually need to check to see if the code is less than 32, between 33 and 46, or between 112 and 123. If it's not in any one of these groups, then you know it's character.

Here's what the handleKeyUp() method looks like so far:

AutoSuggestControl.prototype.handleKeyUp = function (oEvent) {
    var iKeyCode = oEvent.keyCode;

     if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || (iKeyCode >= 112 && iKeyCode <= 123)) {
        //ignore
    } else {
        //autosuggest
    }
};

Now you'll know when a character key is pressed. At that point, you need to begin the autosuggest functionality by calling the suggestion provider's requestSuggestions() method and passing a pointer to the autosuggest control as an argument:

AutoSuggestControl.prototype.handleKeyUp = function (oEvent) {
     var iKeyCode = oEvent.keyCode;

     if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || (iKeyCode >= 112 && iKeyCode <= 123)) {
        //ignore
    } else {
        this.provider.requestSuggestions(this);
    }
};

Remember, it's the suggestion provider that will call the autosuggest() method defined earlier. The requestSuggestions() method begins the process of retrieving suggestions for usage. When the array of suggestions has been built, it will be passed to autosuggest().

With this method defined, it must be assigned as the event handler for the textbox. It's best to create a separate method to handle initializations for the control such as this (there will be more in the future). The init() method serves this purpose:

AutoSuggestControl.prototype.init = function () {
    var oThis = this;
    this.textbox.onkeyup = function (oEvent) {
        if (!oEvent) {
            oEvent = window.event;
        }
        oThis.handleKeyUp(oEvent);
    };
};

The init() method starts by creating a pointer to the this object so that it may be used later. An anonymous function is defined for the textbox's onkeyup event handler. Inside of this function, the handleKeyUp() method is called using the oThis pointer (using this here would refer to the textbox instead of the autosuggest control).

Since this method requires the event object to be passed in, it's necessary to check for both DOM and IE event objects. The DOM event object is passed in as an argument to the event handler while the IE event object is a property of the window object. Instead of doing a browser detect, you can check to see if the oEvent object is passed into the event handler. If not, then assign window.event into the oEvent variable. The oEvent variable can then be passed directly into the handleKeyUp() event handler.

Lastly, the init() method should be called from within the AutoSuggestControl constructor:

function AutoSuggestControl(oTextbox, oProvider) {
    this.provider = oProvider;
    this.textbox = oTextbox;
    this.init();
}

With the autosuggest control completed, it's time to take a look at creating a suggestion provider.

Building a Suggestion Provider

A suggestion provider is nothing more than an object with a method called requestSuggestions(). This method needs to accept a single argument, which is the autosuggest control to work with. The basic format of a suggestion provider is as follows:

function SuggestionProvider() {
    //any initializations needed go here
}

SuggestionProvider.prototype.requestSuggestions = function (oAutoSuggestControl) {

    var aSuggestions = new Array();

    //determine suggestions for the control
    oAutoSuggestControl.autosuggest(aSuggestions);
};

As you can see, there is very little special code in a suggestion provider. You can provide any sort of initialization in the constructor that is necessary; there are no specific rules about it. The requestSuggestions() method needs only to build an array of suggestions and then pass that array into the autosuggest control's autosuggest() method. It's important that an array is passed to autosuggest() even if it's empty; a null value will cause an error.

Suppose you want users to fill in their home state (in the United States) and want to provide suggestions as they type. For this, you would define a suggestion provider that contains an array of state names, such as this:

function StateSuggestions() {
    this.states = [
        "Alabama", "Alaska", "Arizona", "Arkansas",
        "California", "Colorado", "Connecticut",
        "Delaware", "Florida", "Georgia", "Hawaii",
        "Idaho", "Illinois", "Indiana", "Iowa",
        "Kansas", "Kentucky", "Louisiana",
        "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota",
        "Mississippi", "Missouri", "Montana",
        "Nebraska", "Nevada", "New Hampshire", "New Mexico", "New York",
        "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon",
        "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota",
        "Tennessee", "Texas", "Utah", "Vermont", "Virginia",
        "Washington", "West Virginia", "Wisconsin", "Wyoming"
    ];
}

The StateSuggestions constructor creates the array of state names and stores it in a property. This array will be used every time suggestions are made. The only other step is to define requestSuggestions().

Inside of requestSuggestions(), you first need to define the suggestions array and get the current value of the autosuggest control:

StateSuggestions.prototype.requestSuggestions = function (oAutoSuggestControl) {
    var aSuggestions = [];
    var sTextboxValue = oAutoSuggestControl.textbox.value;

    //more code
};

Next, you need to be sure the textbox actually contains text (there's no reason to suggest anything if the textbox is empty). If so, then simply loop through each state name to see if it begins with the text in textbox; the indexOf() method will return 0 if this is the case. When a suggestion is found, it should be added to the suggestions array. Lastly, the suggestions array should be passed into the control's autosuggest() method. Here's the complete code:

StateSuggestions.prototype.requestSuggestions = function (oAutoSuggestControl) {
    var aSuggestions = [];
    var sTextboxValue = oAutoSuggestControl.textbox.value;

    if (sTextboxValue.length > 0){
        for (var i=0; i < this.states.length; i++) {
            if (this.states[i].indexOf(sTextboxValue) == 0) {
                aSuggestions.push(this.states[i]);
            }
        }
        oAutoSuggestControl.autosuggest(aSuggestions);
    }
};

The algorithm used in this method is very generic and can be used for gathering suggestions from any array of values.

Example

With the autosuggest control and a suggestion provider built, it's time to test out your creation. Here's an example:

<html>
    <head>
        <title>Autosuggest Example 1</title>
        <script type="text/javascript" src="autosuggest1.js"></script>
        <script type="text/javascript" src="suggestions1.js"></script>
        <script type="text/javascript">
            window.onload = function () {
                var oTextbox = new AutoSuggestControl(document.getElementById("txt1"), new StateSuggestions());
            }
        </script>
    </head>
    <body>
        <p><input type="text" id="txt1" /></p>
    </body>
</html>

The autosuggest1.js file contains the AutoSuggestControl definition; suggestions1.js contains the StateSuggestions definition. Since the autosuggest control requires a reference to the textbox, you must wait until the page has been completely loaded. So, in the window.onload event handler, an AutoSuggestControl object is created. The textbox to use has an id of "txt1", so a reference to it is easily retrieved using document.getElementById(). A StateSuggestions object is created and immediately passed to the autosuggest control as well. And that's it!

You can view the example here (it should work in Internet Explorer 5.5+ and Mozilla 1.0+, including Firefox) or download it.

The autosuggest control in this article only implements type ahead functionality, which provides a single suggestion for what the user has typed. In the next part, you'll learn how to create a dropdown list of multiple suggestions as well as how to navigate it using the keyboard.

About the Author

Nicholas C. Zakas is a user interface designer for Web applications and the author of Professional JavaScript for Web Developers (Wiley Press, ISBN 0764579088). Nicholas can be contacted through his Web site, http://www.nczonline.net/, where he provides open source JavaScript libraries and tools.