Build ultra-speed autocomplete with Go and jQuery [Part 2]

Build ultra-speed autocomplete with Go and jQuery [Part 2]

In the first part we saw how to create the “backend” side with Go, now we need to write the “frontend” part with jQuery: we’ll use jQuery UI Autocomplete.

1. The basic

In our HTML document we need to include jQuery and then jQuery UI with a bit of style. After jQuery UI, we put also our JS file named script.js. #autocomplete will be our input.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>My awesome autocomplete</title>
    <link href="http://code.jquery.com/ui/1.12.0/themes/smoothness/jquery-ui.css" rel="stylesheet" />
</head>
<body>
    <input type="text" id="autocomplete" />

    <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
    <script src="http://code.jquery.com/ui/1.12.0/jquery-ui.min.js"></script
    <script src="script.js"></script>
</body>
</html>

2. JavaScript

Note: Here we suppose that a reverse proxy is already enabled to retrieve data directly from our Go router.

$('#autocomplete').autocomplete({
    source: function(req, res) {
        $.get('/autocomplete/company/' + req.term.replace(/ /g, '+'), function(r) {
            return res(r);
        }, 'json');
    },
    minLength: 2,
    select: function (e, ui) {
        $(e.target).val(ui.item.name);
        return false;
    }
});

This is the first draw of our autocomplete. In source function, we tell to autocomplete that we need to retrieve the response from /autocomplete/company/{term} and process the output with res(r). Term will be req.term (the input value) with the escape of spaces. In select we set the input value with the selected (or clicked) item of the dropdown.

This is cool, but we want to format the output in the dropdown as we want, beacuse now the autocomplete doesn’t know which parameter from the output is the correct one. For this reason, we need to edit the _renderItem function of the autocomplete. We need to attach a function at the end of the code (before the ;) as following:

}).autocomplete('instance')._renderItem = function (ul, item) {
    return $('<li>')
        .append('<a>' + item.name + '</a>')
        .appendTo(ul);
};

This is simple: we create a <li><a> with inside our name (stored in the item param) and we append it to the main <ul> of the dropdown (retrieved by the ul param).

This is the final code:

$('#autocomplete').autocomplete({
    source: function(req, res) {
        $.get('/autocomplete/company/' + req.term.replace(/ /g, '+'), function(r) {
            return res(r);
        }, 'json');
    },
    minLength: 2,
    select: function (e, ui) {
        $(e.target).val(ui.item.name);
        return false;
    }
}).autocomplete('instance')._renderItem = function (ul, item) {
    return $('<li>')
        .append('<a>' + item.name + '</a>')
        .appendTo(ul);
};

3. Holy cache

It works! But… A request to the server at each character typed? Yes, we set the minLength arg to 2 (min. 2 chars to make a request for the autocomplete), but… We need something… Something like the cache.

Yes, we implemented it already in Go, but what about a “client” cache? We are going to use localStorage (JavaScript native) to store the results. We create two function: autocomplete_from_cache and autocomplete_add_cache. The first method will load autocomplete previsions from cache, the second one with store them.

function autocomplete_add_cache(term, results) {
    var expiry = new Date();
    expiry.setMonth(expiry.getMonth() + 1);
    expiry = expiry.getTime();

    localStorage.setItem('autocomplete-company-' + encodeURI(term.toLowerCase()), JSON.stringify({ expiry: expiry, results: results }));
}
function autocomplete_from_cache(term) {}

The method autocomplete_add_cache is really simple. We need two actions here: create an expiration date and store item in localStorage. For the first we use the Date object of JavaScript, adding 1 month (getMonth() + 1) to the current time and retrieving from it a timestamp; for the second, with localStorage.setItem(key, value) we can put the item in cache. As key, we use the format autocomplete-company-term (with a URL-encoded term), and as value the JSON-encode of the results array.

Now we need to retrieve the cache. In our autocomplete_from_cache, the first thing to do is to verify if localStorage is supported by the current browser. If you want you can read the code, it’s really simple, otherwise just trust me.

function autocomplete_from_cache(term) {
    // Check for local storage
    if (typeof localStorage !== 'undefined') {
        try {
            localStorage.setItem('feature_test', 'yes');
            if (localStorage.getItem('feature_test') === 'yes') {
                localStorage.removeItem('feature_test');
            } else {
                return false;
            }
        } catch(e) {
            return false;
        }
    } else {
        return false;
    }
}

Now we are able to retrieve the results with localStorage.getItem(key) and if there is no match or the cache is expired, we return false.

function autocomplete_from_cache(term) {
    // Check for local storage
    if (typeof localStorage !== 'undefined') {
        try {
            localStorage.setItem('feature_test', 'yes');
            if (localStorage.getItem('feature_test') === 'yes') {
                localStorage.removeItem('feature_test');
            } else {
                return false;
            }
        } catch(e) {
            return false;
        }
    } else {
        return false;
    }

    // Check if item exists
    if (localStorage.getItem('autocomplete-' + type + '-' + encodeURI(term.toLowerCase())) === null)
        return false;

    var autocomplete = JSON.parse(localStorage.getItem('autocomplete-company-' + encodeURI(term.toLowerCase())));
    // Check if is expired
    if (autocomplete.expiry < new Date().getTime()) {
        return false;
    }

    return autocomplete.results;
}

This is it. Simple, right? autocomplete_from_cache will be invoked before the AJAX call, and autocomplete_add_cache inside the callback of the call, to store the results.

// [...]
source: function(req, res) {
    var loadFromCache = autocomplete_from_cache(req.term);
    // Return cache results, if there are
    if (loadFromCache) {
        return res(loadFromCache);
    }

    $.get('/autocomplete/autocomplete/' + req.term.replace(/ /g, '+'), function(r) {
        autocomplete_add_cache(req.term, r);
        return res(r);
    }, 'json');
},
// [...]

Our finally code will look like this. You can get it also from this pastebin.

$('#autocomplete').autocomplete({
    source: function(req, res) {
    var loadFromCache = autocomplete_from_cache(req.term);
    // Return cache results, if there are
    if (loadFromCache) {
        return res(loadFromCache);
    }

        $.get('/autocomplete/company/' + req.term.replace(/ /g, '+'), function(r) {
        autocomplete_add_cache(req.term, r);
        return res(r);
        }, 'json');
    },
    minLength: 2,
    select: function (e, ui) {
        $(e.target).val(ui.item.name);
        return false;
    }
}).autocomplete('instance')._renderItem = function (ul, item) {
    return $('<li>')
        .append('<a>' + item.name + '</a>')
        .appendTo(ul);
};

function autocomplete_add_cache(term, results) {
    var expiry = new Date();
    expiry.setMonth(expiry.getMonth() + 1);
    expiry = expiry.getTime();

    localStorage.setItem('autocomplete-company-' + encodeURI(term.toLowerCase()), JSON.stringify({ expiry: expiry, results: results }));
}

function autocomplete_from_cache(term) {
    // Check for local storage
    if (typeof localStorage !== 'undefined') {
        try {
            localStorage.setItem('feature_test', 'yes');
            if (localStorage.getItem('feature_test') === 'yes') {
                localStorage.removeItem('feature_test');
            } else {
                return false;
            }
        } catch(e) {
            return false;
        }
    } else {
        return false;
    }

    // Check if item exists
    if (localStorage.getItem('autocomplete-' + type + '-' + encodeURI(term.toLowerCase())) === null)
        return false;

    var autocomplete = JSON.parse(localStorage.getItem('autocomplete-company-' + encodeURI(term.toLowerCase())));
    // Check if is expired
    if (autocomplete.expiry < new Date().getTime()) {
        return false;
    }

    return autocomplete.results;
}

If you missed it, check out also Part 1.

Leave a Reply

Your email address will not be published. Required fields are marked *