Blog Article

Extending jQuery UI’s Autocomplete to Improve Success

On Black Book Singles, one of the steps in the signup process is to provide the city and country in which you live. The country part is easy to get right since it’s a simple drop-down list where the user can be sure that the value they’ve selected is correct. The problem most users are encountering is that the city name allows freeform text. This is necessary due to the large number of possible cities available in each country (with over 11,400 cities per country on average).

My initial solution was to convert the city input into an autocomplete field via jQuery UI’s autocomplete plugin. However, I found that a simple autocomplete wasn’t working as well as I’d hoped. After reviewing my first implementation of the autocomplete field, I came to the following conclusion:

  1. The autocomplete field is visually styled the same as other text input fields on the page. There’s nothing indicating to users that they’re going to have to do anything other than type into the field and proceed to the next step.
  2. The jQuery UI autocomplete plugin defaults to a 250ms delay prior to fetching the list of possible values. This can be overridden via the delay option, but setting it to 0 (i.e.: immediately search) is not viable since it will result in extra unnecessary requests to the server while the user is still typing most of the time.
  3. There’s an additional request/response delay after the built-in 250ms delay.

The solution I came up with was to provide hints similar to what the jQuery Tokeninput plugin does:

  1. Immediately show a hint such as “Type in a search term” when the user focuses on the input field.
  2. Immediately update the hint to read “Searching…” when the plugin starts searching for matching values.

So if jQuery Tokeninput already has these features, why not just use that? The problem is that plugin is intended to replace a multiple select input; for my purpose, I’m looking for a substitute for a single select input.

Given this, there are several possible solutions to my problem:

  1. Find a jQuery (or raw JavaScript) autocomplete plugin that already does something similar.
  2. Create a custom autocomplete plugin from scratch that includes this functionality.
  3. Extend jQuery UI’s autocomplete plugin to include this missing functionality.

After spending a good amount of time searching for an existing plugin that does this, I was surprised to find myself empty-handed. It seems as though there’s no good open-source solution that provides this.

Moving onto the second option, I could build my own autocomplete plugin from scratch. This approach seemed fruitless considering the number of tried and true solutions already available for free. Similarly, I briefly considered branching from jQuery UI’s autocomplete plugin, customizing it as a new plugin by editing and adding only what was needed. I tossed this idea aside as well, as it resulted in too much code duplication, and I would miss out on the benefit of updates to jQuery UI.

Finally, I considered the possibility of extending jQuery UI’s autocomplete, only touching the parts that need to be changed and adding the missing pieces from there. This seemed like the best option among the three. A simple search on extending jQuery plugins led to an answer on StackOverflow that explained the basic premise. The raw boilerplate code looks like this:

(function($) {
    $.extend(true, $["ui"]["autocomplete"].prototype, {
        myCustomFunctionA: function() {
            // TODO
        },
 
        myCustomFunctionB: function() {
            // TODO
        },
 
        // etc...
    });
})(jQuery);

This is a great start. From this, I can assume that I’ll want to create a few methods for showing and hiding a hint text DOM element, as well as binding to some user events to have these methods called. However, I would need to make all this happen through the existing initialization happening within jQuery UI autocomplete.

To do this, I needed to replace jQuery UI autocomplete’s _create function with my own, while still allowing the the original version to execute as intended. I recognized, however, that I would have to do this for multiple functions in the plugin; as such, I wanted a solution that would handle all of these cases. My solution was to make backup copies of the original function definitions, referencing them from my replacement definitions. (I’m open to suggestions for improvement on this approach.)

(function($) {
    var plugin = $["ui"]["autocomplete"].prototype;
 
    // store copies of the original plugin functions before overwriting
    var functions = {};
    for (var i in plugin) {
        if (typeof(plugin[i]) === "function") {
            functions[i] = plugin[i];
        }
    }
 
    // extend existing functionality of the autocomplete plugin
    $.extend(true, plugin, {
        _create: function() {
            // call the original plugin _create code
            functions["_create"].call(this);
 
            // TODO create the hint DOM element, and bind
            //      to events for showing/hiding the hint
        }
    }
})(jQuery);

With this, I can take the code to completion by filling in the missing pieces of functionality I originally set out to implement.

(function($) {
    var plugin = $["ui"]["autocomplete"].prototype;
 
    // store copies of the original plugin functions before overwriting
    var functions = {};
    for (var i in plugin) {
        if (typeof(plugin[i]) === "function") {
            functions[i] = plugin[i];
        }
    }
 
    // extend existing functionality of the autocomplete plugin
    $.extend(true, plugin, {
        _create: function() {
            var self = this;
 
            // call the original plugin _create code
            functions["_create"].call(this);
 
            // capture the data-hint string on the input element
            this.hintText = this.element.data("hint");
 
            // set up the hint object
            this.hint = $("<div></div>");
                .addClass("ui-autocomplete-hint")
                .appendTo($(this.options.appendTo || "body", this.element[0].ownerDocument)[0]);
 
            // show/hide hint text on focus/blur
            this.element
                .bind("focus.autocomplete", function(event) {
                    self._showHint(self.hintText);
                })
                .bind("blur.autocomplete", function(event) {
                    self._hideHint();
                });
        },
 
        destroy: function() {
            this.hint.remove();
            functions["destroy"].call(this);
        },
 
        _search: function(value) {
            this._hideHint();
            functions["_search"].call(this, value);
        },
 
        _showHint: function(text) {
            if (text !== "") {
                this.hint.text(text).show();
                this._resizeHint();
            }
        },
 
        _hideHint: function() {
            this.hint.hide();
        },
 
        _resizeHint: function() {
            this.hint
                .outerWidth(this.element.outerWidth())
                .position($.extend({
                    of: this.element
                }, this.options.position));
        }
    }
})(jQuery);

The above example is not the full implementation, as it doesn’t include some cleanup I’ve included as well as the “Searching…” hint functionality I mentioned previously. However, it still demonstrates how to cleanly extend the jQuery UI autocomplete plugin, the basis of which can easily be reused on other jQuery plugins.

To view the full code, visit my jQuery UI Autocomplete Hints extension on Github. Feel free to use the code in your projects, and if you see any opportunities for improvement, send me a pull request to include your changes.

LEAVE A COMMENT

theme by teslathemes