Autocomplete Basics

Chapter 2 28 mins

Learning outcomes:

  1. Basic markup and styles
  2. Adding interactivity
  3. Focus on semantics
  4. Selecting a suggestion
  5. Hiding the suggestions box

Introduction

It's finally time to sit down and begin developing one of the most demanding and useful features on the web to aid in searching - autocomplete.

In the previous chapter, we got a really in-depth overview of what autocomplete is and how are some big names out there using it to give a professional touch to their applications' searching bases.

In this chapter we shall understand the basic skeletal structure of an autocompleter and then code a simple instance of it using minimal subfeatures. Here we're just looking after a functional autocompleter, not a robust library capable of adapting to one's requirements.

Once we fully perceive this foundational structure we'll then head over for a complex journey of tedious subfeatures such as arrow navigation, sorting, grouping, and so on.

So without wasting anymore of our time, let's begin!

How it works?

What we need to first settle down on is that how does an autocompleter work. Without this knowledge we can't even begin thinking of coding it.

In an autocompleter we basically have an input element where could enter stuff. As soon as we type a query into it, it's matched against a predefined list of options created by the developer.

Wherever a match is found (which depends on the algorithm used in the matching phase), the corresponding option is displayed in a suggestions box.

To get an even better idea of all this consider the example below.

Say we have an input element where a user could enter a food item he wants to order at home. And suppose that the list of possible food items is the following:

Pizza
Pasta
Donut
Cake
Cookie

Now the moment the user enters the letter 'p', a loop comes into action. For every food item in the list, the item is matched against 'p' to see if the letter exists anywhere in the item's name.

If it does, the item is put in a results box that is displayed to the user at the end of the loop. In this case 'p' matches 'Pizza' and 'Pasta' and so these two items are displayed in the suggestions box, appearing right below the input element.

Pizza
Pasta

Live Example

This is autocomplete in effect!

Building blocks

If you're an experienced developer, then just by understanding how autocomplete works, you could construct a live instance of it. And it's recommended that you first try coding it on your own and then continue reading from here.

We'll start by noting down all the fundamental requirements of an autocompleter without which it can't be brought to life. This will provide us with a blueprint which we can base our coding on.

An autocompleter essentially requires the following:

  1. An input area where queries could be entered.
  2. A list of options to be considered in the matching part.
  3. A suggestions box where matches of the query could be displayed.

The input area can easily be provided using the HTML <input> element, whereas the results box could be put up using a simple <div> element.

As far as the list of options is concerned, it can be provided using a JavaScript array, owing to the fact that iteration over the array class is extremely easy.

So these are the building blocks of even the most simplest autocompleter on the planet. However, the blocks aren't still connected together into one functional unit.

To do so, we need to employ our old lessons of JavaScript.

Firstly, an event handler needs to track when something is entered in the input area and react at this. The reaction is to iterate over all the elements of the options list and filter out those that match the query.

Each match is transformed into a markup string which is then appended to the suggestions box's innerHTML property. Finally, the suggestions box is displayed.

A user can click on any of the options in the box and get its value to be taken up by the input element.

And in this way, the three individual building blocks of our autocompleter get connected to one another.

The input element tracks the input of data into it, upon which it activates a loop over the list. The list throws out matches of the input query and these matches line up in the results box. The results box is displayed with its suggestions where the user can select any one of them and get the underlying value to be shown in the input element.

With a rock-solid blueprint in hand for our autocompleter, we can now grab the tools and start the practical work - coding!

The HTML and CSS

The first and foremost concern in the coding phase of any component on the web is figuring out its HTML. For our autocompleter we just need two things: an input area and a suggestions box.

Likewise we'll need two elements <input> and <div> to model these, respectively. And since these two elements belong to the autocompleter, we'll group them under a parent element.

<div id="autocomplete">
    <input type="text" id="ac-input">
    <div id="ac-suggestions"></div>
</div>

Both are given ids so that it's easy and convenient to select them using CSS and HTML DOM methods.

The next concern is to give an artistic touch to the elements. This part is entirely upto you how you want to do it.

Both the input and the div element need to have the same width and font size so that they look harmonius when displayed together. The former can easily be done by giving both of them a 100% width.

Furthermore, we'll give a padding to all the individual options in the results, which will also be div elements as we shall see later on. This will make them look more spacious and calm.

#ac-input, #ac-suggestions {
    border: 1px solid #eee;
    width: 100%; /* same width */
    font-size: 18px; /* same font size */
    font-family: sans-serif; /* same font family */
}
                                
#ac-input {
    display: block; /* allow input to change its width */
    box-sizing: border-box; /* keep the padding within 100% width */
    padding: 8px; /* make text in the input element look spacious */
}

#ac-suggestions {
    background-color: #ddd;
    border-top: 0; /* remove top border, since #ac-input has a bottom border already */
}

And with this, our styling work is done.

Last but not the least, it's now time to unravel the logic part!

Adding interactivity

To start with we'll suppose that the list of options of our autocompleter is represented by the array list.

Say that list in this case is as follows:

var list = ["Pizza", "Pasta", "Donut", "Cake", "Cookie"]

With the list created, we are now just left with making our input element and suggestions box interactive.

Below we use HTML DOM methods to select both the input and suggestions box in the variables inputArea and suggestionsBox.

var inputArea = document.getElementById("ac-input"),
     suggestionsBox = document.getElementById("ac-suggestions");

After this we need to handle the keyup event on inputArea to respond to the user's action of typing something into the input field.

Inside the onkeyup handler we'll put a loop that runs over list.

Each element in list is matched against the query typed in inputArea and if a match is found, the corresponding element is enclosed in a markup string which is ultimately appended to resultsBox.innerHTML.

Which of the following methods can be used to search for the query typed in inputArea in a given element of list?

  • indexOf()
  • test()
  • find()
  • slice()

Consider the code below to iterate over the entire length of list.

var q = inputArea.value;
for (var i = 0; i < list.length; i++) {
    if ( /* matching expression */ ) {}
}

What expression should go in the if statement in line 2 such that it returns true if the current element of list has an occurence of q in it.

Note that the expression shall ignore casing i.e 'a' shall match 'Apple', 'A' shall match 'Bat' and so on.

The expression is list[i].toLowerCase().indexOf(q.toLowerCase()) !== -1.

Let's now formally define the onkeyup handler for inputArea combining all the concepts we've established above.

inputArea.onkeyup = function() {
    var q = this.value;

    // clear away previous suggestions
    suggestionsBox.innerHTML = "";

    // figure out matches from list
    for (var i = 0; i < list.length; i++) {
        if (list[i].toLowerCase().indexOf(q.toLowerCase()) !== -1) {
            suggestionsBox.innerHTML += '<div>' + list[i] + '</div>'
        }
    }
}

When we type anything into inputArea, first of all suggestionsBox's innerHTML property is set to an empty string in order to clear it up from any previous suggestions.

Then a loop is executed over list. On each iteration i, if list[i] has q in it (ignoring casing), list[i] is encapsulated in a <div> element and then this is put inside suggestionsBox.innerHTML.

Live Example

This completes the logic of showing suggestions. However, there are still a couple of things missing from the onkeyup handler. These are left as tasks 3 and 4.

It's suggested that suggestionsBox shall be hidden and the onkeyup handler exited, when the value of inputArea turns into an empty string.

Rewrite the onkeyup handler (shown above) to accomplish this.

To hide suggestionsBox you shall set its display property to none.

inputArea.onkeyup = function() {
    var q = this.value;

    // hide suggestions if inputArea becomes empty
    if (q.length === 0) {
        suggestionsBox.style.display = "none"; // hide suggestions
        return; // exit this handler
    }

    // clear away previous suggestions
    suggestionsBox.innerHTML = "";

    // figure out matches from list
    for (var i = 0; i < list.length; i++) {
        if (list[i].toLowerCase().indexOf(q.toLowerCase()) !== -1) {
            suggestionsBox.innerHTML += '<div>' + list[i] + '</div>'
        }
    }
}

Live Example

In the previous code, if a user types something in inputArea that does not exist in list, no message is shown to indicate this.

Rewrite the handler to indicate this idea.

The message needs to be 'No results found'.

inputArea.onkeyup = function() {
    var q = this.value;

    // hide suggestions if inputArea becomes empty
    if (q.length === 0) {
        suggestionsBox.style.display = "none"; // hide suggestions
        return; // exit this handler
    }

    // clear away previous suggestions
    suggestionsBox.innerHTML = "";

    // figure out matches from list
    for (var i = 0; i < list.length; i++) {
        if (list[i].toLowerCase().indexOf(q.toLowerCase()) !== -1) {
            suggestionsBox.innerHTML += '<div>' + list[i] + '</div>'
        }
    }

    if (suggestionsBox.innerHTML === "") {
        suggestionsBox.innerHTML = "No results found!";
    }
}

Live Example

At this point, we've successfully created a partially functional autocompleter. The rest of the functionality lies in selecting a given element in the suggestions list, but before this we need to have a bit of discussion...

Focus on semantics

If you noticed one thing in the last code snippet above, each option in the suggestions box was enclosed inside a <div> element. Although this does serve our need, it fails to make the HTML semantically meaningful.

Semantic HTML is when the markup describes the structure of a document i.e which part is a title, which part is a paragraph, which part requires particular emphasis by the reader, which part is a list element and so on and so forth.

The markup distinguishes between different bits of information in the webpage.

Consider the following HTML of a webpage:

<div class="heading">The heading</div>

<div class="para">This is a paragraph</div>
<div class="para">This is another paragraph, with some <span class="bold">bold text</span>.</div>

<div class="list">
    <div class="list-item">Item 1</div>
    <div class="list-item">Item 2</div>
    <div class="list-item">Item 3</div>
</div>

Although visually one can get the page to describe its structure, markup-wise it's just a collection of <div> elements. A search engine can't differentiate between different blocks of information on this page, which means that to these user agents it's of no practical importance.

If we only replace a couple of tags in this HTML, the overall markup can become way more expressive and sensible.

Consider the code below:

<h1>The heading</h1>

<p>This is a paragraph</p>
<p>This is another paragraph, with some <b>bold text</b>.</p>

<ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
</ul>

The title is in <h1>, the three paragraphs are in <p>, some emphasized text is in <b> and so on. Different bits of information are distinguished from one another, just by means of using different tags to represent them.

This is semantic HTML - more meaningful, both visually and in terms of markup.

Going back to our topic, the suggestions shown in suggestionsBox represented by <div> elements is an instance of not utilising semantic HTML to enliven up a component's information structure.

Each <div> element merely says that this is a block element - nothing else.

What element do you think can better represent each suggestion?

The answer has to do something with lists.
Well, to represent each suggestion we can use an <li> element (of a <ul> element).

The logic will remain the same; just the code for onkeyup will change a bit. And how much of a change will occur, this you shall figure out first!

On each iteration, we enclose the suggestion in an li element and then push this string onto a suggestions array. At the end of the iterations, all elements of suggestions are joined together, and the whole resultant string enclosed in a ul element, which is then assigned to the innerHTML property of suggestionsBox.

Below shown is an illustration of this:

inputArea.onkeyup = function() {
    var q = this.value,
         suggestions = [];

    if (q.length === 0) {
        suggestionsBox.style.display = "none";
        return;
    }

    suggestionsBox.innerHTML = "";

    for (var i = 0; i < list.length; i++) {
        if (list[i].toLowerCase().indexOf(q.toLowerCase()) !== -1) {
            suggestions.push('<li>' + list[i] + '</li>');
        }
    }

    // check if there exist no matches of q
    if (suggestions.length === 0) {
        suggestionsBox.innerHTML = "No results found!";
    }
    else {
        suggestionsBox.innerHTML = '<ul>' + suggestions.join("") + '</ul>'
    }

    suggestionsBox.style.display = "block";
}

Because we're using <ul>, we need to make a few additions to our old CSS in order to remove the default styling of the <ul> and <li> elements:

#ac-suggestions ul {
    list-style-type: none; /* remove each list item's circles */
    padding: 0; /* remove ul's default padding */
    margin: 0 /* remove ul's default margins */
}

Now our code creates a suggestions list using not just HTML but rather semantic HTML. Good for us, good for bots - good for everyone!

Live Example

Another way to code this..

Note that instead of using an array suggestions over here, we can also use a string suggestionsStr and concatenate each suggestion's markup string to it.

inputArea.onkeyup = function() {
    var q = this.value,
         suggestionsStr = "";

    if (q.length === 0) {
        suggestionsBox.style.display = "none";
        return;
    }

    suggestionsBox.innerHTML = "";

    for (var i = 0; i < list.length; i++) {
        if (list[i].toLowerCase().indexOf(q.toLowerCase()) !== -1) {
            suggestionsStr += '<li>' + list[i] + '</li>';
        }
    }

    if (suggestionsStr.length === 0) {
        suggestionsBox.innerHTML = "No results found!";
    }
    else {
        suggestionsBox.innerHTML = '<ul>' + suggestionStr + '</ul>'
    }

    suggestionsBox.style.display = "block";
}

However, concatenation is generally an expensive operation if we compare it to pusing elements onto an array and joining them all at once (just like we did in the previous code), especially in cases where there are thousands of iterations to be done.

Selecting a suggestion

The second last thing left to cover in this chapter is to give the subfeature of selecting a given suggestion by clicking on it. That is, by clicking on a given suggestion, inputArea's value becomes equal to the suggestion's value.

Uptil yet there's no piece of code that can listen to click actions on each element in suggestionsBox, but now we'll be working on this subfeature.

Essentially, we have two choices to handle click events on the <li> elements of suggestionsBox:

  1. One is to give each element a click handler that holds that selection logic.
  2. The second is to give a click handler to suggestionsBox and use the event's argument object to check if the click's target is an <li> element.

We'll go with the latter, since it's more flexible and efficient than having to define a separate click handler for each <li> element in the suggestions list.

Consider the following code:

suggestionsBox.onclick = function(e) {
    // if clicked on a suggestion, select it
    if (e.target.nodeName === "LI") {
        inputArea.value = e.target.innerHTML;
        suggestionsBox.style.display = "none";
    }
}

We've given an onclick handler to suggestionsBox so that whenever a click is made within it, the handler comes into action. This handler checks the target that dispatched the click event, and if it is an <li> element, it puts the suggestion in inputArea.

Once a suggestion is clicked, it means that the user has decided what he wanted to enter into the input element and now there's no need to show suggestionsBox anymore. Consequently, we set suggestionsBox's display property to none (in line 5).

To give the impression that an <li> element is clickable, we give it a :hover rule, as shown below:

#ac-suggestions li:hover {
    background-color: orange
}

As the pointer is brought over it, its background color changes.

Simple as cake!

Live Example

Note how we've put the clicked element's innerHTML into inputArea.value (in line 4). At least in this case, this shortcut will work; however when we move into complex areas of our autocompleter, we'll see that it won't.

If each suggestion contains a food item along with its price and description, we can't just take the innerHTML of a clicked suggestion and put it into inputArea. Rather we would have to see which position does the clicked suggestion lie on in the array list and then extract out its name from there. We'll see this in the coming chapters.

Hiding all suggestions

Let's say, after typing a value in inputArea, at which a suggestions list is shown to, you click somewhere outside the input element. What do think should happen at this moment?

Generally, in autocompleters around the net, the suggestions box is hidden. In this section we shall go over the details and implementation of this subfeature in our very own autocompleter.

We'll start with deciding the event to be handled...

Which event do we need to handle in order to track when a click is made outside inputArea?

  • click
  • focus
  • blur

The options for the event are a handful. We've got click, blur, mousedown, mouseup etc. However, the most efficient and simplest of all is blur.

The blur event fires when a given input element loses focus i.e when we click outside it while it has focus.

In constructing the code to hide suggestionsBox, we can use the onblur handler of inputArea and hide suggestionsBox from within it.

In the first sight it might seem very easy to accomplish this, as can be seen below:

inputArea.onblur = function(e) {
    suggestionsBox.style.display = "none";
}

Simply hide suggestionsBox when the input loses focus.

However, note that this comes with a problem:

If the target of the blur event is suggestionsBox itself, even then it's hidden.

When we click outside the input area, in the suggestions box, we are basically interacting with the box; and likewise closing it would be senseless.

In short, we have to address this case differently - do nothing if the blur's target is suggestionsBox.

Now before we present one solution to you, think on this problem and how you would solve it. Get those engines inside your brain to work and give a solution to you!

Let's start by first discussing the steps to take to solve this issue...

The whole idea is that if the blur event fires on inputArea because of a click inside suggestionsBox then the box shouldn't be hidden. In all other cases it should be hidden.

In solving this, a clever developer will utilise the fact that the mousedown event fires before the blur event.

So one can simply see if the mousedown event that happened before inputArea's blur event originated in the suggestions box. If it did, the box shouldn't be hidden; and otherwise it should be.

Whose mousedown event to listen to - shall it be window or some other element.

Well listening to the mousedown event on window would be completely inefficient - it's only suggestions box whom we're concerned with, then why track the mouse event on every thing on the document.

The most appropriate target of mousedown is the suggestionsBox element. In its handler we need to do just one simple thing - tell our script that the suggestions box has been clicked and therefore on blur of the input element it shouldn't be hidden.

Now try to come up with the code to implement this.

The preventDefault() method of the mousedown event sits at the core of the solution.

Below we demonstrate the solution.

We start by defining the onblur handler of inputArea:

inputArea.onblur = function(e) {
    suggestionsBox.style.display = "none";
}

Not surprisingly, with only this in place, if we click (outside the input element) in the suggestions box, the blur event will fire and consequently the box will be hidden.

However, we will prevent this blur event from firing if a mousedown event fires inside suggestionsBox. How? Using preventDefault():

suggestionsBox.onmousedown = function(e) {
    e.preventDefault();
}

As we've stated earlier, the mousedown event fires before blur in the same thread. Thereby, calling preventDefault() within mousedown's handler will prevent any blur event from occuring.

In our case, if the mouse goes down on suggestionsBox, the blur event won't fire on inputArea and so nothing will be hidden.

A simple, yet cunning, solution!

Live Example