56 votes

Comment créer une combobox d'auto-complétion ?

Quelqu'un connaît-il la meilleure façon de créer une combobox d'autocomplétion avec les modèles Knockout JS ?

J'ai le modèle suivant :

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

Parfois, cette liste est longue et j'aimerais que Knockout joue bien avec l'autocomplétion de jQuery ou un code JavaScript direct, mais je n'ai pas eu beaucoup de succès.

En outre, jQuery.Autocomplete nécessite un champ de saisie. Une idée ?

120voto

RP Niemeyer Points 81663

Voici un binding jQuery UI Autocomplete que j'ai écrit. Il est destiné à refléter le options , optionsText , optionsValue , value Le paradigme de liaison utilisé avec les éléments de sélection avec quelques ajouts (vous pouvez demander des options via AJAX et vous pouvez différencier ce qui est affiché dans la zone de saisie de ce qui est affiché dans la zone de sélection qui s'affiche.

Il n'est pas nécessaire de fournir toutes les options. Il choisira les valeurs par défaut pour vous.

Voici un exemple sans la fonctionnalité AJAX : http://jsfiddle.net/rniemeyer/YNCTY/

Voici le même échantillon avec un bouton qui le fait se comporter plus comme une boîte combo : http://jsfiddle.net/rniemeyer/PPsRC/

Voici un exemple avec les options récupérées 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 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());    
    }
};

Vous l'utiliseriez comme :

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

MISE À JOUR : Je maintiens une version de cette reliure ici : https://github.com/rniemeyer/knockout-jqAutocomplete

45voto

Epstone Points 597

Voici ma solution :

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

Utilisation :

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

http://jsfiddle.net/7bRVH/214/ Comparé aux RP's, il est très basique mais peut répondre à vos besoins.

13voto

George Mavritsakis Points 2778

Élimination nécessaire....

Ces deux solutions sont excellentes (celle de Niemeyer étant beaucoup plus fine) mais elles oublient toutes deux le traitement des déchets !

Ils devraient gérer les éliminations en détruisant jquery autocomplete (éviter les fuites de mémoire) avec ceci :

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

4voto

Antonio Inacio Points 41

Améliorations mineures,

Tout d'abord, ces conseils sont très utiles, merci de les partager.

J'utilise la version postée par Epstone avec les améliorations suivantes :

  1. Afficher l'étiquette (au lieu de la valeur) lorsque l'on appuie sur le haut ou le bas - apparemment, cela peut être fait en traitant l'événement focus

  2. Utilisation d'un tableau d'observables comme source de données (au lieu d'un tableau)

  3. Ajout du gestionnaire de jetables comme suggéré par George

http://jsfiddle.net/PpSfR/

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

En fait, spécifier minLength comme 0 permet d'afficher les alternatives en déplaçant simplement les touches fléchées sans avoir à saisir de texte.

2voto

chomba Points 11

J'ai essayé La solution de Niemeyer avec JQuery UI 1.10.x, mais la boîte de saisie automatique n'apparaissait pas. Après quelques recherches, j'ai trouvé une solution de contournement simple ici . L'ajout de la règle suivante à la fin de votre fichier jquery-ui.css règle le problème :

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

J'ai également utilisé Knockout-3.1.0, j'ai donc dû remplacer ko.dependentObservable(...) par ko.computed(...)

En outre, si votre modèle de vue KO contient une valeur numérique, veillez à modifier les opérateurs de comparaison : de === à == et de !== à != , afin que la conversion de type soit effectuée.

J'espère que cela aidera d'autres personnes

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X