web-dev-qa-db-fra.com

Comment créer une liste déroulante à remplissage automatique?

Est-ce que quelqu'un connaît le meilleur moyen de créer une liste déroulante à complétion automatique avec des 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 fonctionne bien avec peut-être une saisie semi-automatique de jQuery ou un code JavaScript simple, mais sans grand succès.

De plus, jQuery.Autocomplete nécessite un champ de saisie. Des idées?

56
Craig Bruce

Voici une liaison jQuery UI Autocomplete que j'ai écrite. Il est destiné à refléter le paradigme de liaison options, optionsText, optionsValue, value utilisé avec des éléments sélectionnés avec quelques ajouts (vous pouvez rechercher des options via AJAX et ce qui est affiché dans la zone de saisie. s'affiche dans la boîte de sélection qui apparaît.

Vous n'avez pas besoin 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 exemple avec un bouton qui le fait se comporter davantage comme une boîte à options: 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'" />

UPDATE: Je maintiens une version de cette liaison ici: https://github.com/rniemeyer/knockout-jqAutocomplete

120
RP Niemeyer

Voici ma 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/ Comparé aux RP, il est très basique mais répond peut-être à vos besoins.

44
Epstone

Élimination nécessaire ....

Ces deux solutions sont excellentes (Niemeyer étant beaucoup plus fin), mais elles oublient toutes les deux le traitement des déchets!

Ils doivent gérer les éliminations en détruisant jquery autocomplete (empêchant 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");
    });
}
13
George Mavritsakis

Améliorations mineures,

Tout d’abord, voici quelques conseils très utiles, merci à tous pour le partage.

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

  1. Affiche le libellé (au lieu de la valeur) en appuyant en haut ou en bas - apparemment, cela peut être fait en gérant l'événement focus

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

  3. Ajout du gestionnaire jetable comme suggéré par George

http://jsfiddle.net/PpSfR/

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

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

4
Antonio Inacio

Correction de la suppression du problème d'entrée sur la charge pour la solution de RP. Même si c'est une sorte de solution indirecte, j'ai changé cela à la fin de la fonction:

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

pour ça:

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

J'ai essayé la solution de Niemeyer avec JQuery UI 1.10.x, mais la boîte de saisie semi-automatique ne s'est tout simplement pas présentée. 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ésout le problème:

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

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

De plus, si votre modèle KO View contient des valeurs numériques, veillez à modifier les opérateurs de comparaison: de === à == et! == à! =, Afin que la conversion de type soit effectuée. 

J'espère que cela aide les autres

2
chomba

Je sais que cette question est ancienne, mais je recherchais également une solution très simple pour notre équipe, qui l’utilisait sous une forme, et découvris que jQuery autocomplete déclenche un événement 'autocompleteselect'

Cela m'a donné cette idée.

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

Avec le gestionnaire étant simplement:

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

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

J'ai aimé cette approche car elle simplifie le gestionnaire et n'attache pas les événements jQuery à mon modèle de vue . Voici un violon avec un tableau au lieu d'une URL comme source. Cela fonctionne si vous cliquez sur la zone de texte et si vous appuyez sur Entrée.

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

0
avid

La solution de Niemeyer est excellente, mais je rencontre un problème lorsque j'essaie d'utiliser la saisie semi-automatique dans un mode modal. La saisie semi-automatique a été détruite lors de la fermeture modale (Erreur non capturée: impossible d'appeler des méthodes avant la fin de la saisie; tentative d'appel de la méthode 'option'). Je l'ai corrigée en ajoutant deux lignes à la méthode subscribe de la liaison:

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