Autocomplete with KnockoutJS

3rd February, 2016 - Posted by david

Recently I started a new job at a company that is looking to transition away from a customised, unstructured, jQuery module set up to use KnockoutJS and RequireJS for it’s modules. This approach was chosen because the core platform is based on Magento and the forthcoming Megento 2 uses KnockoutJS heavily throughout it’s frontend templates. As a good starting point and proof of concept, we decided to look at converting our existing custom-autocomplete module from a combination of EJS and jQuery to pure KnockoutJS. Luckily for me, I was the one who got to implement it, and thus learn a new skill!

I’m not going to go into the ins and outs of how KnockoutJS works but in short it’s a MVVM system, where you have Models, Views and ViewModels, the latter being the interface between the other 2, the client and the server. This autocomplete was a standard input field, whereby on typing 3 characters, an AJAX call is made to the server looking for strings that matched the search string and displayed a clickable list of results underneath the input field. Additionally, you could use the arrow keys to select items in the menu, as well as the mouse. We also have different instances of the autocomplete, to search for different types of entities (e.g. searching for a product vs. search for a place), so we need the code to work with each.

From this point on I’m going to assume at least a basic knowledge of KnockoutJS, how it uses



The View Model

So, first up we’ll want an Autocomplete viewModel, to handle the DOM events in the view (e.g.


etc.), fetch data from the server and call the correct model to format the received data. It’ll have 2 observable attibutes:


, an array of suggestion objects, and


, the incoming query from the user. As a parameter we’ll pass it the model type to format the suggestions (e.g.


below) and we’ll have functions to fetch suggestions as JSON from the server (


), add them to our


array (


, formatting the data via the model along the way) and clear our array (


), as well a helper function to look for valid character key presses (


). None of this is overly complex and it’s well commented, so I’ll just leave the whole class here:

 * AutoComplete viewModel. Handles the observable events from the view, requests data from the server and calls
 * the corresponding Model above to format fetched data
 * @param  options  JSON object of options, to contain:
 *                  - url: URL to request the search results from
 *                  - suggestionEntry: required model (i.e. one of the above) to format the data

function AutoComplete(options) {

    // KnockoutJS standard is to refer to 'self' instead of 'this' throughout the class.
    // It's because 'this' in a sub-function refers to the function, not the viewModel
    var self = this;

    $.extend(self, options);

    // Array to store suggestions received from the server
    self.suggestions = ko.observableArray([]);

    // Value of input field that user queries
    self.q = ko.observable('');

    // Attribute to store the current AJAX request. Means we can cancel the current request if the observable 'q' changes
    self.ajaxRequest = null;

     * Append a JSON search result to our suggestions array. Instantiates the correct model to format the data
     * (view is rendered automatically by KnockoutJS)
     * @param  suggestion  JSON object, returned from search server

    self.addSuggestion = function (suggestion) {
        self.suggestions.push(new self.suggestionEntry(suggestion, self.q()));

     * If the user has entered a valid search string (more than 3 latin-ish or punctuation characters), cancel the current AJAX request (if any),
     * fetch the data from the server, format it and store in 'suggestions' array
     * @param  obj  HTML <input> element (not used)
     * @param  event  The event object for the triggered event (keydown)

    self.loadSuggestions = function(obj, event) {
        // if a valid, non-control, character has been typed
        if (self.validKey(event)) {
            self.clearAjaxRequest(); // cancel current request
            var q = self.q();
            // if they've entered less than 3 characters, just clear the array, which clears the suggestions drop down
            if (q.length < 3) {

            // request data from the server
            self.ajaxRequest = $.getJSON(self.url, {term: q}, function(response) {
                self.clearSuggestions(); // clear out current values
                for (var i = 0; i < response['suggestions'].length; i++) {
                    self.addSuggestion(response['suggestions'][i]); // add search result

    self.clearSuggestions = function() {

    self.clearAjaxRequest = function() {
        if (self.ajaxRequest) {
            self.ajaxRequest = null;

     * Check what key was pressed is valid: if it was alphanumeric, space, punctuation or backspace/delete

    self.validKey = function(event) {
        var keyCode = event.keyCode ? event.keyCode : event.which;
        // 8 is backspace, 46 is delete
        return keyCode == 8 || keyCode == 46 || /^[a-zA-Z0-9\s\-_\+=!"£$%^&*\(\)\[\]\{\}:;@'#~<>,\.\/\?ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž]$/.test(event.key);

We also store the current AJAX request with the object in the


attribute. By doing this, we can cancel any existing requests as the user types more keys. So, when they type the first 3 characters, a request is fired off; when they type the 4th character, we’ll cancel the existing request if it hasn’t finished and do a new search for the longer string.

The Model(s)

For this example, I mentioned above that the user could be searching for locations or products; let’s go with a location search for this example. Below, we have a class


, which takes a JSON object that was returned from our server, formats the matched string by wrapping


tags around the bit of the string that was matched (via the global


function, which I unfortunately don’t have the code for), generates the URL for the result and translates the type of location found (.e.g city, county etc.).

  * Model for a location search result. Formats data to be displayed in the HTML view
  * @param  data  JSON object
  * @param  q     User's original query

function LocationSuggestionEntry(data, q) {
    this.type = (data.type === 'region' && Number( > 999) ? 'country' : data.type;

    var separator = data.url.indexOf('?') !== -1 ? '&' : '?';
    this.url = data.url + separator + 'autocomplete=1&ac-text=' + q; =;

    this.label = data.label;
    // wrap what the user typed in a <strong> tag
    var regexp = new RegExp('(' + accentInsensitiveRegex(q) + ')', 'gi');
    this.labelFormatted = data.label.replace(regexp, '<strong>$1</strong>');

    this.typeTranslated = Translator.translate('autocomplete::type::' + this.type);

The View

So, for the HTML side, we need an


field for the user’s query and a


for the search results. The


will obviously be hidden if we’ve no results to show. We wrap the whole thing in a


with class


, which we’ll use when binding the whole thing together later.

For the


field, we bind our




attribute to Knockout’s


data binding (

textInput: q

), so that every time the value of the




will too. Additionally, we want to fire our


function, which will check the length of


and fetch suggestions from the server if it’s greater than 3 characters; this is achieved by calling


when a Javascript


event is fired on the



event: {keyup: loadSuggestions}


The HTML for the


is also fairly straight-forward. If we have any suggestions to show, we want to add the


class to the



css: {'has-results': suggestions().length > 0}

) and of course hide the


when there’s less than 3 characters typed in the



visible: q().length > 2

). Assuming we have suggestions to show, we loop through the


array, displaying an


for each, containing the suggestion’s




, as well as adding some attributes to the surrounding



data-bind="attr: {href: url ...


<div class="autocomplete">
    <input class="search-input" data-bind="textInput: q, event: {keyup: loadSuggestions}" type="text" name="text" placeholder="Enter a location..."/>
    <ul class="autocomplete-results" data-bind="css: {'has-results': suggestions().length > 0}, visible: q().length > 2">
        <!-- ko foreach: suggestions -->
            <a data-bind="attr: {href: url, 'data-type': type, 'data-id': id, 'data-label': label}">
                <span data-bind="html: labelFormatted"></span> - <span data-bind="text: translatedType"></span>
        <!-- /ko -->

Binding It All Together

At this point we need to bind our


object to our


, passing it the search URL and suggestion type as parameters:

$('.autocomplete').each(function() {
    ko.applyBindings(new AutoComplete({
        url: '/suggestions/locations/',
        suggestion: LocationSuggestion
    }), this);

One last bit…

To get all this working nicely, you’ll need CSS for the


and it’s


children. Additionally, you might want code to look out for when the up and down arrows are pressed on the keyboard and highlight the next row correctly. The code I have for this isn’t mine, so I’m not going to put it here. However, I will point out that to add any fancy jQuery on your view, i.e. to handle these up/down arrow keypress events, you can use KnockoutJS’s custom binding handlers. This is to keep business and presentation logic separate from each other. So, in JS you’d have something like:

 * This custom binding is how knockout lets you set up your HTML elements. It's separate from the viewModel, which
 * should purely deal with business logic, not display stuff.
 * Sample usage: <input type="text" data-bind="autoComplete">

ko.bindingHandlers.autoComplete = {
     * Called when the HTML element is instantiated

    init: function(element, valueAccessor, allBindings, viewModel) {
        var $el = $(element);

        // specific jQuery code goes here
    update: function() {} // not needed here

The HTML for your


would change to

&lt;ul class="autocomplete-results" data-bind="autoComplete, css: {'has-results': suggestions ...

– note the addition of


on the



Tags: javascript knockoutjs | david | 3rd Feb, 2016 at 22:31pm | No Comments

No Comments

Leave a reply

You must be logged in to post a comment.