How to create an auto-complete combobox? How to create an auto-complete combobox? jquery jquery

How to create an auto-complete combobox?


Here is a jQuery UI Autocomplete binding that I wrote. It is intended to mirror the options, optionsText, optionsValue, value binding paradigm used with select elements with a couple of additions (you can query for options via AJAX and you can differentiate what is displayed in the input box vs. what is displayed in the selection box that pops up.

You do not need to provide all of the options. It will choose defaults for you.

Here is a sample without the AJAX functionality: http://jsfiddle.net/rniemeyer/YNCTY/

Here is the same sample with a button that makes it behave more like a combo box: http://jsfiddle.net/rniemeyer/PPsRC/

Here is a sample with the options retrieved via AJAX: http://jsfiddle.net/rniemeyer/MJQ6g/

//jqAuto -- main binding (should contain additional options to pass to autocomplete)//jqAutoSource -- the array to populate with choices (needs to be an observableArray)//jqAutoQuery -- function to return choices (if you need to return via AJAX)//jqAutoValue -- where to write the selected value//jqAutoSourceLabel -- the property that should be displayed in the possible choices//jqAutoSourceInputValue -- the property that should be displayed in the input box//jqAutoSourceValue -- the property to use for the valueko.bindingHandlers.jqAuto = {    init: function(element, valueAccessor, allBindingsAccessor, viewModel) {        var options = valueAccessor() || {},            allBindings = allBindingsAccessor(),            unwrap = ko.utils.unwrapObservable,            modelValue = allBindings.jqAutoValue,            source = allBindings.jqAutoSource,            query = allBindings.jqAutoQuery,            valueProp = allBindings.jqAutoSourceValue,            inputValueProp = allBindings.jqAutoSourceInputValue || valueProp,            labelProp = allBindings.jqAutoSourceLabel || inputValueProp;        //function that is shared by both select and change event handlers        function writeValueToModel(valueToWrite) {            if (ko.isWriteableObservable(modelValue)) {               modelValue(valueToWrite );              } else {  //write to non-observable               if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['jqAutoValue'])                        allBindings['_ko_property_writers']['jqAutoValue'](valueToWrite );                }        }        //on a selection write the proper value to the model        options.select = function(event, ui) {            writeValueToModel(ui.item ? ui.item.actualValue : null);        };        //on a change, make sure that it is a valid value or clear out the model value        options.change = function(event, ui) {            var currentValue = $(element).val();            var matchingItem =  ko.utils.arrayFirst(unwrap(source), function(item) {               return unwrap(item[inputValueProp]) === currentValue;              });            if (!matchingItem) {               writeValueToModel(null);            }            }        //hold the autocomplete current response        var currentResponse = null;        //handle the choices being updated in a DO, to decouple value updates from source (options) updates        var mappedSource = ko.dependentObservable({            read: function() {                    mapped = ko.utils.arrayMap(unwrap(source), function(item) {                        var result = {};                        result.label = labelProp ? unwrap(item[labelProp]) : unwrap(item).toString();  //show in pop-up choices                        result.value = inputValueProp ? unwrap(item[inputValueProp]) : unwrap(item).toString();  //show in input box                        result.actualValue = valueProp ? unwrap(item[valueProp]) : item;  //store in model                        return result;                });                return mapped;                            },            write: function(newValue) {                source(newValue);  //update the source observableArray, so our mapped value (above) is correct                if (currentResponse) {                    currentResponse(mappedSource());                }            }        });        if (query) {            options.source = function(request, response) {                  currentResponse = response;                query.call(this, request.term, mappedSource);            }        } else {            //whenever the items that make up the source are updated, make sure that autocomplete knows it            mappedSource.subscribe(function(newValue) {               $(element).autocomplete("option", "source", newValue);             });            options.source = mappedSource();        }        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {            $(element).autocomplete("destroy");        });        //initialize autocomplete        $(element).autocomplete(options);    },    update: function(element, valueAccessor, allBindingsAccessor, viewModel) {       //update value based on a model change       var allBindings = allBindingsAccessor(),           unwrap = ko.utils.unwrapObservable,           modelValue = unwrap(allBindings.jqAutoValue) || '',            valueProp = allBindings.jqAutoSourceValue,           inputValueProp = allBindings.jqAutoSourceInputValue || valueProp;       //if we are writing a different property to the input than we are writing to the model, then locate the object       if (valueProp && inputValueProp !== valueProp) {           var source = unwrap(allBindings.jqAutoSource) || [];           var modelValue = ko.utils.arrayFirst(source, function(item) {                 return unwrap(item[valueProp]) === modelValue;           }) || {};                    }        //update the element with the value that should be shown in the input       $(element).val(modelValue && inputValueProp !== valueProp ? unwrap(modelValue[inputValueProp]) : modelValue.toString());        }};

You would use it like:

<input data-bind="jqAuto: { autoFocus: true }, jqAutoSource: myPeople, jqAutoValue: mySelectedGuid, jqAutoSourceLabel: 'displayName', jqAutoSourceInputValue: 'name', jqAutoSourceValue: 'guid'" />

UPDATE: I am maintaining a version of this binding here: https://github.com/rniemeyer/knockout-jqAutocomplete


Here is my solution:

ko.bindingHandlers.ko_autocomplete = {    init: function (element, params) {        $(element).autocomplete(params());    },    update: function (element, params) {        $(element).autocomplete("option", "source", params().source);    }};

Usage:

<input type="text" id="name-search" data-bind="value: langName, ko_autocomplete: { source: getLangs(), select: addLang }"/>

http://jsfiddle.net/7bRVH/214/Compared to RP's it is very basic but maybe fills your needs.


Disposal needed....

Both of those solutions are great (with Niemeyer's being much more fine grained) but they both forget the disposal handling!

They should handle disposals by destroying jquery autocomplete (prevent memory leakages) with this:

init: function (element, valueAccessor, allBindingsAccessor) {  ....      //handle disposal (if KO removes by the template binding)    ko.utils.domNodeDisposal.addDisposeCallback(element, function () {        $(element).autocomplete("destroy");    });}