Autocomplete

As part of a new UI library, I have put together an autocomplete feature using the jQuery UI Autocomplete in a manner that follows the guidelines found in the Webgrown Solutions UI Manifesto. The Autocomplete supports the following features:

  • Load suggestions from remote service.
  • Can set delay on remote call to avoid producing a lot of load for remote data, and being less responsive.
  • Can set minimum character needed to make remote call, to avoid large, timely results.
  • Simple item display option.
  • Templated item display option, which allows for the display of multiple data values for each item.
  • Ability to specify which of the multiple data values to use when populating the field on select.
  • Optional highlighting of search term in results (uses <mark> tag).
  • Optional function call on select.
  • UI Manifesto compliance:

The Demo

Simple Display, With No Highlighting

Simple Display, With Highlighting

Templated Display, With No Highlighting

Templated Display, With Highlighting & Select Action

A google search will be made on the person name selected.

The Markup

Just a basic page <input type="text"> element, with a normal class attribute and some custom data (data-*) attributes to allow feature customization. Once the CSS and Script stuff are in place on a website, any page can implement the Autocomplete by simply setting the input element with the class attribute to "autocomplete", and setting the desired custom data attributes.

That sounds like a DRY solution to me (see UI Manifesto).

 
<h3>Simple Display, With No Highlighting</h3>
<p>
    <label for="txtAutocompleteDemo1">Person:</label> 
    <input type="text" id="txtAutocompleteDemo1" class="autocomplete"
        data-serviceRequestUrl="/WebServices/DemoClientService.svc/PersonAutocomplete1"
        data-serviceRequestDelay="300"
        data-serviceRequestMinLength="2"
        data-highlightMatches="false" />
</p>
<h3>Simple Display, With Highlighting</h3>
<p>
    <label for="txtAutocompleteDemo2">Person:</label> 
    <input type="text" id="txtAutocompleteDemo2" class="autocomplete"
        data-serviceRequestUrl="/WebServices/DemoClientService.svc/PersonAutocomplete1"
        data-serviceRequestDelay="300"
        data-serviceRequestMinLength="2"
        data-highlightMatches="true" />
</p>
<h3>Templated Display, With No Highlighting</h3>
<p>
    <script id="personTemplate" type="text/x-jquery-tmpl">
        <div class="itemName">${FullName}</div>
        <div class="itemDetails">${City}, ${StateAbbreviated} ${ZipCode}</div>
    </script>
    <label for="txtAutocompleteDemo3">Person:</label> 
    <input type="text" id="txtAutocompleteDemo3" class="autocomplete"
        data-serviceRequestUrl="/WebServices/DemoClientService.svc/PersonAutocomplete2"
        data-serviceRequestDelay="300"
        data-serviceRequestMinLength="2"
        data-itemTemplateId="personTemplate"
        data-propertyToUseOnSelect="FullName"
        data-highlightMatches="false" />
</p>
<h3>Templated Display, With Highlighting & Select Action</h3>
<p>A google search will be made on the person name selected.</p>
    <label for="txtAutocompleteDemo4">Person:</label> 
    <input type="text" id="txtAutocompleteDemo4" class="autocomplete"
        data-serviceRequestUrl="/WebServices/DemoClientService.svc/PersonAutocomplete2"
        data-serviceRequestDelay="300"
        data-serviceRequestMinLength="2"
        data-itemTemplateId="personTemplate"
        data-propertyToUseOnSelect="FullName"
        data-highlightMatches="true"
        data-selectActionFunction="googlePerson" />
</p>

The Script

//Enable Autocomplete(s)
lastErroredCallTrace += "->autocomplete";
$(parentElementSelector + " .autocomplete").each(function () {
    var tempThisVar = $(this);
    self.setTimeout(function () { enableFeature_Autocomplete(tempThisVar); }, 1);
});

function enableFeature_Autocomplete(targetElement) {
    lastErroredCallTrace += "->enableFeature_Autocomplete";

    //Check to see if already enabled.
    if (!isAttrDefined(targetElement.attr("data-autocompleteEnabled"))) {
        targetElement.autocomplete({
            //source: targetElement.attr("data-serviceRequestUrl"),
            source: function (request, response) {
                lastErroredFunctionName = "common.js -> $(parentElementSelector + .autocomplete).each(function () {";
                lastErroredRequestingElement = targetElement;
                lastErroredServiceRequestUrl = this.element.attr("data-serviceRequestUrl");
                lastErroredJsonToPost = "{ \"term\":\"" + request.term + "\"}";

                makesXhr = $.getJSON(this.element.attr("data-serviceRequestUrl"), { term: request.term }, function (data, textStatus, jqXHR) {
                    if (textStatus == "success") {
                        var dataTemp;
                        if (data.d == undefined) {
                            dataTemp = data;
                        }
                        else {
                            //WCF sends back with the dumb data.d as a string
                            dataTemp = $.trim(data.d);
                            dataTemp = $.parseJSON(dataTemp);
                        }

                        response(dataTemp);
                    }
                    else {
                        errorOnAjaxRequest(jqXHR, textStatus, "Error on Ajax Complete");
                    }
                });
            },
            open: function (event, ui) { if (!targetElement.is(":focus")) { targetElement.delay(500).autocomplete("close"); } },
            minLength: ((!isAttrDefined(targetElement.attr("data-serviceRequestMinLength")) || isNaN(targetElement.attr("data-serviceRequestMinLength"))) ? 2 : eval(targetElement.attr("data-serviceRequestMinLength"))),
            delay: ((!isAttrDefined(targetElement.attr("data-serviceRequestDelay")) || isNaN(targetElement.attr("data-serviceRequestDelay"))) ? 300 : eval(targetElement.attr("data-serviceRequestDelay"))),
            select: function (event, ui) {
                if (isAttrDefined(targetElement.attr("data-propertyToUseOnSelect")) && targetElement.attr("data-propertyToUseOnSelect").length > 0) {
                    targetElement.val(ui.item[targetElement.attr("data-propertyToUseOnSelect")]);
                }
                else {
                    targetElement.val(ui.item.value);
                }
                if (isAttrDefined(targetElement.attr("data-selectActionFunction")) && targetElement.attr("data-selectActionFunction").length > 0) {
                    callFunctionDynamically(targetElement.attr("data-selectActionFunction"), targetElement, ui.item);
                }
                return false;
            }
        })
            .data("autocomplete")._renderItem = function (ul, item) {
                if (isAttrDefined(this.element.attr("data-itemTemplateId")) && this.element.attr("data-itemTemplateId").length > 0) {
                    var itemTemplate = $("#" + this.element.attr("data-itemTemplateId")).html();

                    var itemVariables = itemTemplate.match(/\${([^}]*)}/gi); //Get "${...} vars.
                    for (x in itemVariables) {
                        if (x >= 0) {
                            var itemVariableFull = itemVariables[x];
                            var itemVariableNameOnly = itemVariableFull.substr(2, (itemVariableFull.length - 3));
                            var itemVariableValue = item[itemVariableNameOnly];

                            if (itemVariableValue != undefined && itemVariableValue.length > 0) {
                                if (this.element.attr("data-highlightMatches") == "true") {
                                    var regExp = new RegExp(this.term, "i");
                                    var match = regExp.exec(itemVariableValue);
                                    itemVariableValue = itemVariableValue.replace(regExp, markOpenTag + match + markCloseTag);
                                    itemTemplate = itemTemplate.replace(itemVariableFull, itemVariableValue);
                                }
                                else {
                                    itemTemplate = itemTemplate.replace(itemVariableFull, itemVariableValue);
                                }
                            }
                        }
                    }
                }
                else {
                    if (this.element.attr("data-highlightMatches") == "true") {
                        var regExp = new RegExp(this.term, "i");
                        var match = regExp.exec(item.label);
                        itemTemplate = item.label.replace(regExp, markOpenTag + match + markCloseTag);
                    }
                    else {
                        itemTemplate = item.label;
                    }
                }

                var anchorElement = document.createElement("a");
                anchorElement.className = "itemWrapper clearFix";
                anchorElement.innerHTML = itemTemplate;

                return $(liElementConstructor)
			    .data("item.autocomplete", item)
			    .append(anchorElement)
			    .appendTo(ul);
            };

        //Mark as enabled.
        targetElement.attr("data-autocompleteEnabled", true);
    }
}

The CSS

 
/* General Autocomplete */
ul.ui-autocomplete li { border-top:1px dashed #CCC; }
ul.ui-autocomplete li:first-child { border-top-width:0px; }
ul.ui-autocomplete mark { background-color:Yellow; font-style:inherit; font-weight:inherit; }

/* Make Autocomplete Scrollable */
.ui-autocomplete { max-height:300px; overflow-y:auto; /* prevent horizontal scrollbar */ overflow-x:hidden; /* add padding to account for vertical scrollbar */ padding-right:20px; }
* html .ui-autocomplete { height:300px; }

<!-- The jQuery UI styling of your choice -->
<link href="jquery-ui.css" rel="stylesheet" type="text/css" />