Skip to content

Instantly share code, notes, and snippets.

@IcodeNet
Created January 21, 2014 08:06
Show Gist options
  • Save IcodeNet/8536100 to your computer and use it in GitHub Desktop.
Save IcodeNet/8536100 to your computer and use it in GitHub Desktop.
How do I use Knockout and Chosen?
/*
binding format
data-bind = "table: {
chosenOption: *** or {***} //the option passed to chosen function when create the chosen
source: ***, //source items
valueProp: *** //the property of the items in source that will be used as the value of the option
selectedValue: *** //the value that user is selected, it can be array
selectedValueItemProp: the prop of the items in selectedValue array, that will be used to match the value in source
displayProp: the property of the items in source that will be used as the text of the option, it also can be a valid expression, the "this" is current item, and we can also use $parent, $parents, $data and $root
}"
*/
(function () {
var _ = {
UO: ko.utils.unwrapObservable
};
_.CO = ko.bindingHandlers.chosen = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
if ($(element).prop("tagName") !== "SELECT") {
throw "chosen binding can be applied only for select";
}
$(element).addClass("chosen-select");
var value = valueAccessor();
_.CO.updateSelect(element, value, bindingContext, viewModel);
var chosen;
//this is a bug of chosen, that is if the html havn't been show, then the chosen control (div) will be set as 0px width
//to fix this, we have manually get the original select width and use it as the option to set chosen
var chosenOpt = { width: $(element).outerWidth() + "px" };
if (value.chosenOption) {
chosenOpt = $.extend(chosenOpt, _.UO(value.chosenOption));
}
chosen = $(element).chosen(chosenOpt);
//hack: if the any parent of chosen css is set to "overflow: hidden",
//and the parent height is less to show the full chosen input and drop down, then the drowdown can't be show
//here we add a div in side the form, as the root container, then it is ok
if ($(element).parent()) {
//on a modal
}
chosen.change(function (event, data) {
_.CO.updateValue(value, $(element).val(), element, allBindingsAccessor);
//if (ko.isObservable(value.selectedValue)) {
// value.selectedValue(data.selected);
//} else {
// throw 'selectedValue must be bound to an observable field';
// //we throw this exception is becasue ,value is the binding value, will will copy from the specified property of viewModel
// //and if it is not a observable, then just value copy, which will not change the copied property of viewMode
// //value.selectedValue = data.selected;
//}
});
},
//set the bound view model property value to the selected value
updateValue: function (databoundValue, selectedOptionValue, element, allBindingsAccessor) {
//the value of option is alwasy the index so we must convet it to the corresponding data item
if ($.isArray(selectedOptionValue) && databoundValue.selectedValueItemProp) {
throw "Not supported yet";
}
var selectedOptionValueArray = [];
if ($.isArray(selectedOptionValue)) {
selectedOptionValueArray = selectedOptionValue;
} else {
selectedOptionValueArray.push(selectedOptionValue);
}
var tmpSetValueArray = [];
for (var i = 0; i < selectedOptionValueArray.length; i++) {
if (typeof (selectedOptionValueArray[i]) === "undefined" || selectedOptionValueArray[i] == null || selectedOptionValueArray[i] == "") {
continue;
}
var optionValue = $(element).children("[value=" + selectedOptionValueArray[i] + "]").data("item");
if (typeof (_.UO(databoundValue.valueProp)) !== "undefined") {
optionValue = _.UO(optionValue[_.UO(databoundValue.valueProp)]);
}
tmpSetValueArray.push(optionValue);
}
if (ko.isObservable(databoundValue.selectedValue)) {
//if multi select, then the target must be an observable array, so just update it with the array
if ($(element).prop("multiple")) {
databoundValue.selectedValue(tmpSetValueArray);
} else {
databoundValue.selectedValue(tmpSetValueArray[0]);
}
////if multi select, then the target must be an observable array, so just update it with the array
//if (tmpSetValueArray.length > 1) {
// databoundValue.selectedValue(tmpSetValueArray);
//} else {
// //check if target is an array
// if (databoundValue.selectedValue.push) {
// databoundValue.selectedValue(tmpSetValueArray);
// } else {
// databoundValue.selectedValue(tmpSetValueArray[0]);
// }
//}
} else {
throw 'selectedValue must be bound to an observable field';
//we throw this exception is becasue ,value is the binding value, will will copy from the specified property of viewModel
//and if it is not a observable, then just value copy, which will not change the copied property of viewMode
//databoundValue.selectedValue = selectedOptionValue;
}
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
_.CO.updateSelect(element, valueAccessor(), bindingContext, viewModel);
//for the case, if the selected value is not any option, but the chose is set to single select mode, then it will select the frist value, but won't trigger change event
//so we force the value are always same between the element and the viewmodel
_.CO.updateValue(valueAccessor(), $(element).val(), element, allBindingsAccessor);
$(element).trigger("chosen:updated");
},
updateSelect: function (element, bindData, bindingContext, viewModel) {
element = $(element);
element.empty();
$(element).prepend("<option></option>");
var value = _.UO(bindData);
var source = _.UO(value.source);
var valueProp = _.UO(value.valueProp);
var selectedValue = _.UO(value.selectedValue);
var displayProp = _.UO(value.displayProp);
var selectedValueItemProp = _.UO(value.selectedValueItemProp);
var selectedValueArray = [];
if ($.isArray(selectedValue)) {
selectedValueArray = selectedValue;
} else {
selectedValueArray.push(selectedValue);
}
function inArray(array, arrayItemProp, searchValue) {
for (var j = 0; j < array.length; j++) {
var item = array[j];
if (arrayItemProp) {
item = item[arrayItemProp];
}
if (item == searchValue) {
return true;
}
}
return false;
}
for (var i = 0; i < source.length; i++) {
var sourceItemValue = _.UO(source[i]);
if (valueProp) {
sourceItemValue = _.UO(source[i][valueProp]);
}
var displayValue = _.UO(source[i]);
if (displayProp) {
if ((displayProp in displayValue)) {
displayValue = _.UO(displayValue[displayProp]);
} else {
displayValue = (function () {
var func;
if (element[0][displayProp]) {
func = element[0][displayProp];
} else {
var funcBody = "with ($bindingContext){with($bindingContextOverride) {with ($currentItem) {return " + displayProp + ";}}}"
element[0][displayProp] = func = new Function("$bindingContext", "$currentItem", "$bindingContextOverride", funcBody);
}
var bindingContextOverride = {
$parents: bindingContext.$parents.slice(0),
$data: displayValue,
$parent: viewModel
};
bindingContextOverride.$parents.splice(0, 0, viewModel);
return func(bindingContext, displayValue, bindingContextOverride);
})();
}
} else { //if displayProp is empty, it means directly show the current item
}
var opt;
if (inArray(selectedValueArray, selectedValueItemProp, sourceItemValue)) {
opt = $('<option selected="selected" value="' + i + '">' + displayValue + '</option>');
} else {
opt = $('<option value="' + i + '">' + displayValue + '</option>');
}
opt.data("item", _.UO(source[i]));
element.append(opt);
}
}
};
})();
<select data-placeholder="Select item" data-bind="chosen: { chosenOption: { allow_single_deselect: true }, source: clusters, valueProp: 'Id', selectedValue: selectedCluster, displayProp: 'Name' }"></select>
<!--
where clusters is an observable array of objects with properties Id,Name
source: clusters,
valueProp: 'Id',
selectedValue: selectedCluster,
displayProp: 'Name'
-->
@RobertoPrevato
Copy link

An extremely complicated solution to a simple problem. It is sufficient to use the regular data binders "options", "optionsText", "optionsValue", "value" that are already included in KnockOut, so that KnockOut builds the option elements. Then call .chosen() in the init of a custom bindingHandler, and trigger the "chosen:updated" event when it's needed.

In fact, this code solves the same problems: "valueProp" is the equivalent of KnockOut "optionsValue",
"displayProp" is the equivalent of Knockout "optionsText", "source" is the equivalent of "options" data binder,
selectedValue the equivalent of "value", except that doesn't support simple arrays, it requires observable arrays...

Best regards,
Roberto Prevato

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment