AJAX Search Suggest (WeAreHunted.com Style)

View on GitHub

WeAreHunted.com is a music website with some great functionality that makes it unique yet highly usable. In particular, I find the way in which a user can search the site to be very intuitive and it opens up the process to be a lot more useful. The site uses the full browser window to present the user with suggestions that closely match what they are looking for. In this month's tutorial, we're going to create our own version of WeAreHunted.com's full screen search suggestion feature using jQuery, AJAX and PHP.

Getting Started

To recreate the search suggest feature, we're going to use jQuery and a small amount of PHP to query the server for search terms. To allow these two scripts to communicate, we'll make use of AJAX -- passing a string containing the search term from jQuery to PHP. The PHP will then return a string containing the results. We'll make use of JSON (JavaScript Object Notation) so our search terms/results are well formatted and easily encoded/decoded between these two technologies.

Note that for the sake of simplicity, we're going to use a PHP file with some search terms in it that we'll pretend are actual results. In the real world, you'd query a database.

The HTML

Let's begin by creating the interface itself. Quite simply, to launch the search feature, we just need a search button, like so:

<div id="search">
    Search
    <img src="images/bt-search.jpg" alt="Search" />
</div>

We've given the div an ID of "search" which will allow us to add a click event to it in the jQuery later on.

When the user clicks the search button, we'll open an overlay that sits on top of the rest of the page.

<div id="search-overlay">
    <h2>Begin typing to search</h2>
    <div id="close">X</div>
    <form>
        <input id="hidden-search" type="text" autocomplete="off" />
        <input id="display-search" type="text" autocomplete="off" readonly="readonly" />
    </form>

    <div id="results">
        <h2 class="artists">Artists</h2>
        <ul id="artists"></ul>
    </div>
</div>

Again, we've given the containing div an ID to allow us to interact with it via jQuery. We've also added another div with the ID of "close", which, you guessed it, will close the overlay when clicked.

The search overlay also contains a form with not one, but two input boxes. Why? As WeAreHunted.com is a very visual site, they've gone to a lot of effort to make the site appear exactly as they want, meaning a small amount of trickery using two search input boxes to remove the blinking cursor from the input box. We'll go into greater detail on this in the the CSS.

We also have a "results" div within the overlay that contains a list of the terms that match the users search query.

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="scripts/ajax-search-suggest.js"></script>
<link rel="stylesheet" type="text/css" media="screen" href="css/styles.css" />

Finally for the HTML, we need to reference the jQuery library (hosted via Google), and the jQuery and CSS files we'll be creating shortly.

The CSS

To begin, we'll style the search button (the div with an ID of "search"):

#search {
    background: #d3d3d3;
    cursor: pointer;
    font-size: 24px;
    font-weight: bold;
    text-transform: uppercase;
    padding: 20px 2%;
    width: 96%;
}

Obviously, you can style your search button however you want -- none of the above CSS is required for the script to work correctly.

The overlay that will appear when we click the search button is styled like so:

#search-overlay{
    background: black;
    background: rgba(0,0,0,0.85);
    color: white;
    display: none;
    font-size: 24px;
    height: 100%;
    padding: 20px;
    position: fixed;
    width: 100%;
}

Let's take a look at this a little closer. We've given the overlay a background colour of black but also specified an RGBA background too. So, in newer browsers the background will appear black (RGB: 0, 0, 0) but also have an alpha opacity of 0.85 and the page behind the overlay still shows through slightly. Older browsers will simply show a solid black background.

The overlay has a position of "fixed" which means it will also appear within the viewport and the height/width of 100% makes sure it covers the whole viewport. Also note that we've set the overlay not to show using "display: none". We'll change this in the jQuery when a user hits the search button so it becomes visibile.

Now, let's take a look at those two input boxes and understand why we need two of them:

<input id="hidden-search" type="text" autocomplete="off" />
<input id="display-search" type="text" autocomplete="off" readonly="readonly" />

If you click on an input box in a browser, you'll notice a blinking text cursor. This is something WeAreHunted.com appear not to have wanted. Unfortunately, there's not a CSS property we can simply apply to make it go away, so instead, we need to use two input boxes. One input box will be hidden off screen ("hidden-search") which is where the user will type, and the second input box ("display-search") will mirror the contents of the first box. However, the second box is set to "readonly" in the HTML, meaning the blinking cursor is removed. Each input box also turns off "autocomplete" to prevent previous search queries from appearing in a list below.

Let's hide the "hidden-search" input box in the CSS:

#hidden-search{
    left: -10000px;
    position: absolute;
}

Here we set the input boxes position to absolute and -10000px. This will place the box way over to the left, a long way from the viewport, where the user can't see it.

#display-search{
    background: none;
    border: none;
    color: white;
    font-size: 60px;
    margin: 25px 0 0 0;
    width: 960px;
}

Now we can style the mirrored search input box. Nothing special is required here other than simple styling.

Our demo also contains a close button which is styled like so:

#close{
    cursor: pointer;
    position: fixed;
    right: 20px;
    top: 20px;
}

The only other notable styles are for the results list:

#results{
    display: none;
    width: 300px;
}

As we did with the overlay, we'll hide the results list until we need it, using "display: none".

The jQuery

Now we have our interface set up, let's start adding the functionality using jQuery.

Showing/Hiding the Overlay

$(document).ready(function(){
    //...
}

We will wrap the code in jQuery's document ready function. This will initiate as soon as the HTML and CSS has rendered (and the page is ready).

Once the document is ready, we'll select some of the elements that will be used more than once; the two input boxes, the search overlay and the list of artists:

var $hiddenSearch = $("#hidden-search"),
    $displaySearch = $("#display-search"),
    $searchOverlay = $("#search-overlay"),
    $artistsList = $("#artists");

Note that we're saving these selectors to variables to improve performance (it's good practice to save a selector you will use more than once).

Now we add our first click event to the search button:

$("#search").click(function(){
    $searchOverlay.show();
    $hiddenSearch.focus();
});

When the "search" button is clicked, the "search-overlay" is shown (the CSS is changed from "display: none" to "display: block") and we give the input box with ID of "hidden-search" focus. "Giving focus" is what happens when a user clicks on an input box -- that box now has focus and the user can begin typing in it. Here we're controlling the focus ourselves so as soon as the search button is clicked, the user can begin typing. Remember that the "hidden-search" input box is hidden off-screen so it's very important we apply the focus because the user isn't able to click the box themselves.

$("#search-overlay").click(function(event){
    $hiddenSearch.focus();
    if(event.target.id == "search-overlay" || event.target.id == "close"){
        $hiddenSearch.blur();
        $(this).animate({"opacity": 0}, 500, function(){
            $(this).hide().css("opacity", 1);
        });
    }
});

We want the "search-overlay" to close when clicked, but not in all cases. When applying click events in jQuery or JavaScript, the element it's applied to also listens to click events on its child elements. In this case, the "search-overlay" will have quite a few children; the close button, the input boxes and the list of results. The close button should obviously close the "search-overlay" when clicked, but we don't want the input boxes or results list to close the search overlay.

A child notifying it's parent of an event is called "bubbling" or "propagation". As we want to be selective about which elements "bubble" we can pass the event parameters into the function and query those parameters, like so:

$("#search-overlay").click(function(event){
    //...
    if(event.target.id == "search-overlay" || event.target.id == "close"){
        //...
    }
}

By passing the event, we can query exactly which element or child element is being clicked. Here we've set up an if statement to determine whether the element being click is either the "search-overlay" or "close" button.

Now we can put all of our code to be executed when these two elements are clicked, into that if statement:

$hiddenSearch.blur();
$(this).animate({"opacity": 0}, 500, function(){
    $(this).hide().css("opacity", 1);
});

So, if the "search-overlay" or "close" button are clicked, we remove the focus from the "hidden-search" input by using .blur(). We then fade out the "search-overlay" and hide it.

Note that before we used the if statement to determine what element was being clicked, we added the focus to the "hidden-search" input again:

$hiddenSearch.focus();

This is to make sure that if the user was clicking on an element that didn't hide the "search-overlay", the "hidden-search" input would still have focus (overriding whatever element the browser tried to apply focus to when the user clicked on the page).

Key Events

For our demo, we will use the "keydown" event, meaning whenever a user pushes down on a keyboard key, we can trigger our AJAX call to the PHP script, as well as dealing with the two input boxes we've created.

$hiddenSearch.keydown(function(e){
    //...
}

Note for this keydown event -- like the click event on the "search-overlay" -- we're passing the event into the function (this time I've called it "e" just for speeding up writing the following code, and in real world code, I'd call it "e" consistently -- although that's personal preference, the choice is yours).

currentQuery = $displaySearch.val();

As soon as the user hits a key on their keyboard, we're going to grab the query they've typed into the "display-search" input box and save it as a variable called "currentQuery".

Now to make use of that event we passed into the function...

if(e.keyCode == 8){
    //...
}

Here we get the keyCode from the event ("e"). Each key has a value, in the case of the above code, 8 is the backspace key. See this reference sheet for a list of key codes. So, this if statement is checking to see if the user has hit the backspace key (and is trying to delete whatever is contained in the input box).

Let's skip what code is triggered when the backspace key is pressed for the moment and take a look at what happens when the user hits the alphanumberic keys (A - Z, 0 - 9):

if(e.keyCode == 8){
    //...when the user hits the backspace key (we'll write this functionality shortly)
}else if(e.keyCode == 32 || (e.keyCode >= 65 && e.keyCode <= 90) || (e.keyCode >= 48 && e.keyCode <= 57)){
    latestQuery = currentQuery + String.fromCharCode(e.keyCode);
    displaySearch.val(latestQuery);
    updateResults(latestQuery);
}

Key codes 32, 65 - 90 and 48 - 57 are the space bar, alphabetic keys A - Z, and numeric keys 0 - 9. Whenever the user hits one of these keys, we add the value of that key to the end of whatever the user has already added to the search input box, like this:

latestQuery = currentQuery + String.fromCharCode(e.keyCode);

If the user has so far typed "AC", when they hit the next key ("D" for example), the "currentQuery" is "AC" and the "String.fromCharCode(e.keyCode)" is "D", making the latestQuery "ACD".

"e.keyCode" will return the value of the key and by using "String.fromCharCode()", we can turn that value in the actual key being pressed.

$displaySearch.val(latestQuery);

For each key the user hits, we construct their query "latestQuery" and add it into the "display-search" input box.

When using key events and determining what keys the user is hitting, you usually don't have to construct the contents and add it into the input box yourself, but remember we're having to hack the input boxes a little because we wanted to remove the blinking text cursor.

Finally for the alphanumeric key down event, we have this last line of code:

updateResults(latestQuery);

This is the final function in our keycode event that passes the "lastestQuery" to the PHP to try and find a match for the users query. Let's return to the backspace key event before going into that:

if(e.keyCode == 8){
    latestQuery = currentQuery.substring(0, currentQuery.length - 1);
    $displaySearch.val(latestQuery);
    updateResults(latestQuery);
}

Now we know how the alaphanumeric keys work, this should look familiar. On the first line, we construct the "latestQuery" again but this time, when the user hits the backspace key, we remove the last character from the latest query. Once that's done, we update the "display-search" input box again and trigger the "updateResults" function to return results for the user.

Returning Results using AJAX

Let's take a look at the "updateResults" function now, which is triggered whenever the user alters the contents of the input box:

function updateResults(latestQuery){
    if(latestQuery.length > 0){
        //...
    }
}else{
    $artistsList.html("<li>No results</li>");
}

First and foremost, we make sure the user has actually entered a query by checking its length. If they haven't, we update the artists list to show there weren't any results. The "updateResults" function will only execute when the user has pressed a key so the only time the "latestQuery" length won't be above 0 is when the user has completely deleted their query.

Now here's where we use AJAX and start communicating with the PHP script:

if(latestQuery.length > 0){
    $.post("auto-suggest.php", {latestQuery: latestQuery}, function(data){
        //...do stuff with the returned results
    });
}

We're posting to the PHP script "auto-suggest.php" (which we'll look at the contents of shortly) the "latestQuery" variable which contains the users search query. We then trigger a function which passes the "data" or the result that is returned from the "auto-suggest.php" script. Within that function we will update and modify the results list based on that new result which we'll write shortly, but for now, let's follow the flow of the code and take a look at the PHP.

The PHP

The php file "auto-suggest.php" exists on the server. For the sake of this demo, we're going to include a list of static results to query but in the real world, you'd connect to and query a database.

<?php
    include ("test-data.php");
    //...
?>

The test data looks like this:

$data = array(
    "ABBA" => "?search=ABBA",
    "ACDC" => "?search=ACDC",
    //...and so on
    );

Each result is made up of a term ("ABBA") and a URL ("?search=ABBA"). By including the "test-data.php" file, we can now query the list of artists from our main PHP file "auto-suggest.php".

include ("test-data.php");
if(isset($_POST['latestQuery'])){
    //...
}

So once our data is included, we can test to see if the "latestQuery" variable has been posted to the PHP script (and it will have been via AJAX).

$latestQuery = $_POST['latestQuery'];
$latestQueryLength = strlen($latestQuery);
$result = array();

To make things a little easier to read, we'll turn that posted "latestQuery" into a PHP variable and count how many characters make up that query.

It's possible -- particularly in a real world application -- that the user's query will return more than one result so we've also set up an array by the name of "result", that can hold multiple terms that match the user's query.

foreach($data as $name => $url){
    if (substr(strtolower($name),0,$latestQueryLength) == strtolower($latestQuery)){
        $result[$name] = $url;
    }
}

Now, for each item in our list of test data, we're going to break up the item into variables, one for the term ("ABBA"), the other for its URL ("?search=ABBA"). We'll also use an if statement to determine if the users query is contained within that term. If there is a match, we store the term and its URL within the results array. The term is used as the key and the URL its value, like so:

ABBA : "?search=ABBA"

Once the loop that matches queries to terms is complete, we finally "echo" the result array after encoding it as JSON:

echo json_encode($result);

"Echoing" the result returns it back to the jQuery script. By encoding it as JSON, we turn the whole array into a string which is easily passed between the PHP and jQuery via AJAX. If this seems a little overwhelming, imagine the jQuery and PHP scripts being the start and end points of a journey, AJAX is the road between the two points and JSON is the vehicle -- containined within is the message we need to deliver between the two.

Back to the jQuery

PHP has now done it's job and returned either an empty string (no matching terms), or a string containing atleast one matching term.

if(data.length > 0){
    data = $.parseJSON(data);
    //...code to output results
}else{
    $artistsList.html("<li>No results</li>");
}

Here we determine if the PHP script returned a result or not by checking its length. When we "echoed" the results from the PHP script, we converted them to JSON.

If there isn't a result, we add "No results" to the list of results. If there is atleast one result, we parse the JSON -- turning it into a JavaScript object -- and save that into a variable.

Let's assume the user has so far typed "AC". Querying the test data, PHP will return two results, which when parsed will look like this:

{ACDC : "?search=ACDC", Ace of Spades : "?search=AceOfSpades"}

We can now use this string to output and update the results list.

$("#artists li").remove(":contains('No results')");
$("#results").show();

Firstly, we do some housework. In case the results list is already showing "No results", we'll remove that from the list. In the CSS, we also hid the results list, so if that is yet to be displayed, we'll change its CSS from "display: none", to "display: block".

previousTerms = new Array();

Now, an easy way to update the list of results would be to clear the list everytime a new result is returned and populate it again. This causes some undesirable flashing during the refreshing of the list though. Instead, what we're going to do, is create an array of terms, called "previousTerms". We will then compare the previous terms (the ones currently being displayed) with the new ones returned from the PHP script.

$("#artists li").each(function(){
    previousTerms.push($(this).text());
});

For each list item in the list of results, we will add the text from that item, to the "previousTerms" array.

Let's continue with our example; the user has so far typed "AC" and the PHP script has returned results. We'll also assume that the list of results has so far shown each result for the query "A". So the results we are yet to display, look like this:

{ACDC : "?search=ACDC", Ace of Spades : "?search=AceOfSpades"}

And the "previousTerms" array looks like this:

["ABBA" , "ACDC, "Ace of Spades"]

As we're not just going to clear the whole list of results and output the new ones, we need to compare the new terms with the previous terms.

keepTerms = new Array();

Let's create a new array that will hold only the terms we want to keep.

for(term in data){
    url = data[term];
    if($.inArray(term, previousTerms) === -1){
        $artistsList.prepend('<li><a href="'+url+'" title="'+term+'">'+term+'</a></li>');
    }else{
        keepTerms.push(term);
    }
}

Now we can begin breaking up that string of returned results. "term" itself is the key (the name of the artists) and "data[term]" is the value (the URL). For each key/value pair, we're going to compare the term with the terms in the "previousTerms" array. If it isn't in the array, we add the term to the list of results (wrapped in a little bit of HTML to create the link). If it is in the "previousTerms" array (and is already being displayed), we'll add it to the "keepTerms" array.

We're almost there! Before we write the final step --removing the terms from the results list that no longer match the latest returned results -- we need to make sure the AJAX is keeping up with the users speed of typing.

One thing to note about sending lots of querires off to a server based script is that it takes time for that to happen. We're sending a request every time the user hits a key, which means they're most likely changing the query quicker than the AJAX can return results. To make sure the most current result is being displayed for the most recent query, we'll use another if statement:

if(data == "" || (keepTerms.length == 0 && (previousTerms.length != 0 || $displaySearch.val() == ""))){
    $artistsList.html("<li>No results</li>");
}else{
    //...code to execute removal of irrelavent results
}

In any of these instances, we want to show "no results":

  • the PHP script has returned no results, or
  • the "keepTerms" array is empty, and:

    • the "previousTerms" array is not empty, or
    • the search input box is empty

If you remove this if statement from the code and quickly change the query, you'll see that the returned results do not always reflect the most current query. This final if statement rectifies that issue.

So, providing none of the above are true and there are results to show, we can execute the last bit of code.

We have two final arrays to compare, the "previousTerms" (those being displayed currently), and "keepTerms" (only the ones that are now relevant).

Again, let's take a look at our example. The user has queried "AC" so far, and previously we displayed results for the query "A". "The previousTerms" array looks like this:

["Ace of Spades", "ACDC", "ABBA"]

And the "keepTerms" array like this:

["Ace of Spades", "ACDC"]

By comparing these two arrays, we can single out the terms that no longer match the returned terms and remove them:

for(term in previousTerms){
    if($.inArray(previousTerms[term], keepTerms) === -1){
        $("#artists li").filter(function(){
            return $(this).text() == previousTerms[term]
        }).remove();
    }
}

For each term in the "previousTerms" array, we determine if that term also exists in the "keepTerms" array, if it doesn't, find it in the list of results and remove it.

Conclusion

As mentioned previously, this demo queries a simple array of test data -- in the real world, you'd query a database and could use better matching algorithms provided by SQL.

If you plan on using this in a real world application, I'd also recommend taking performance considerations further. It may be a good idea to limit the number of results returned as well as constructing the list of results in a variable so they can be outputted as one instead of using jQuery to output each list item individually.

This code could be quickly adapted to consider those real world factors and hopefully it's helped you to understand how simple it is to get jQuery and PHP to communicate with each other via AJAX.

Remember you can view the demo here, download the code here, fork AJAX Search Suggest (WeAreHunted.com style) on GitHub, or comment/ask a question below.

Useful? Buy me a coffeeUseful? Buy me a coffee

Ian Lunn is a Front-end Developer with 12 years commercial experience, author of CSS3 Foundations, and graduate of Internet Technology. He creates successful websites that are fast, easy to use, and built with best practices.