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?
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
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.
É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");
});
}
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:
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
Utilisation d'un tableau observable comme source de données (au lieu d'un tableau)
...
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.
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);
}
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
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.
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);
});