如何创建一个自动完成组合框?

时间:2011-09-24 04:22:48

标签: javascript jquery knockout.js

有没有人知道使用Knockout JS模板创建自动完成组合框的最佳方法?

我有以下模板:

<script type="text/html" id="row-template">
<tr>
...
    <td>         
        <select class="list" data-bind="options: SomeViewModelArray, 
                                        value: SelectedItem">
        </select>
    </td>
...        
<tr>
</script>

有时这个列表很长,我想让Knockout很好地使用jQuery自动完成或一些直接的JavaScript代码,但收效甚微。

此外,jQuery.Autocomplete需要输入字段。有什么想法吗?

9 个答案:

答案 0 :(得分:120)

这是我写的jQuery UI Autocomplete绑定。它旨在反映与select元素一起使用的optionsoptionsTextoptionsValuevalue绑定范例以及几个附加内容(您可以通过AJAX和您查询选项)可以区分输入框中显示的内容与弹出的选择框中显示的内容。

您无需提供所有选项。它会为您选择默认值。

以下是没有AJAX功能的示例:http://jsfiddle.net/rniemeyer/YNCTY/

以下是带有按钮的相同示例,使其更像组合框:http://jsfiddle.net/rniemeyer/PPsRC/

以下是通过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 value
ko.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());    
    }
};

您可以使用它:

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

更新:我在这里维护此绑定的一个版本:https://github.com/rniemeyer/knockout-jqAutocomplete

答案 1 :(得分:44)

这是我的解决方案:

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

用法:

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

http://jsfiddle.net/7bRVH/214/ 与RP相比,它非常基本但可能满足您的需求。

答案 2 :(得分:13)

需要处置......

这两种解决方案都很棒(Niemeyer的粒度更精细),但他们都忘记了处理方式!

他们应该通过破坏jquery自动完成(防止内存泄漏)处理处理:

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

答案 3 :(得分:4)

小改进,

首先,这些是一些非常有用的提示,谢谢大家分享。

我正在使用 Epstone 发布的版本,并进行了以下改进:

  1. 向上或向下按下时显示标签(而不是值) - 显然这可以通过处理焦点事件来完成

  2. 使用可观察数组作为数据源(而不是数组)

  3. 按照 George
  4. 的建议添加了一次性处理程序

    http://jsfiddle.net/PpSfR/

    ...
    conf.focus = function (event, ui) {
      $(element).val(ui.item.label);
      return false;
    }
    ...
    

    顺便说一句,将 minLength 指定为0只允许通过移动箭头键来显示备选方案,而无需输入任何文本。

答案 4 :(得分:2)

我尝试使用JQuery UI 1.10.x Niemeyer's solution,但是自动完成框根本没有显示,经过一些搜索我发现了一个简单的解决方法here。将以下规则添加到jquery-ui.css文件的末尾可以解决问题:

ul.ui-autocomplete.ui-menu {
  z-index: 1000;
}

我也使用了Knockout-3.1.0,所以我不得不用ko.computed(...)替换ko.dependentObservable(...)

此外,如果您的KO View模型包含一些数值,请确保更改比较运算符:从===到==和!==到!=,以便执行类型转换。

我希望这有助于其他人

答案 5 :(得分:2)

修复了RP解决方案的加载问题清除输入。尽管这是一种间接解决方案,但我在函数结束时对此进行了更改:

$(element).val(modelValue && inputValueProp !== valueProp ?
unwrap(modelValue[inputValueProp]) : modelValue.toString());

到此:

var savedValue = $(element).val();
$(element).val(modelValue && inputValueProp !== valueProp ?  unwrap(modelValue[inputValueProp]) : modelValue.toString());
if ($(element).val() == '') {
   $(element).val(savedValue);
}

答案 6 :(得分:0)

Niemeyer的解决方案很棒,但是当我尝试在模态中使用自动完成时遇到了一个问题。自动完成在模态关闭事件中被销毁(未捕获错误:无法在初始化之前调用自动完成方法;尝试调用方法'选项')我通过在绑定的订阅方法中添加两行来修复它:

mappedSource.subscribe(function (newValue) {
    if (!$(element).hasClass('ui-autocomplete-input'))
         $(element).autocomplete(options);
    $(element).autocomplete("option", "source", newValue);
});

答案 7 :(得分:0)

我知道这个问题已经过时了,但我也在寻找一个非常简单的解决方案,让我们的团队在表单中使用它,并发现jQuery autocomplete raises an 'autocompleteselect' event

这给了我这个想法。

<input data-bind="value: text, valueUpdate:['blur','autocompleteselect'], jqAutocomplete: autocompleteUrl" />

处理程序只是:

ko.bindingHandlers.jqAutocomplete = {
   update: function(element, valueAccessor) {
      var value = valueAccessor();

      $(element).autocomplete({
         source: value,
      });
   }    
}

我喜欢这种方法,因为它使处理程序变得简单,并且它不会将jQuery事件附加到我的viewmodel中。 这是一个数组而不是url作为源。如果单击文本框,并且按Enter键,则此方法有效。

https://jsfiddle.net/fbt1772L/3/

答案 8 :(得分:0)

Epstone原始解决方案的另一个变体。

我尝试使用它,但也发现仅在手动输入值时才更新视图模型。选择一个自动完成条目会使视图模型留下旧值,这有点令人担心,因为验证仍然通过 - 只有当您在数据库中查看问题时才会这样做!

我使用的方法是将jquery UI组件的select处理程序挂钩到knockout绑定init中,这只是在选择值时更新knockout模型。此代码还包含了上述乔治的有用答案中的配置管道。

&#13;
&#13;
init: function (element, valueAccessor, allBindingsAccessor) {

        valueAccessor.select = function(event, ui) {
            var va = allBindingsAccessor();
            va.value(ui.item.value);
        }

        $(element).autocomplete(valueAccessor);

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

    }
...
&#13;
                    <input class="form-control" type="text" data-bind="value: ModelProperty, ko_autocomplete: { source: $root.getAutocompleteValues() }" />
&#13;
&#13;
&#13;

这现在运作得很好。它旨在对照页面上预加载的值数组,而不是查询api。