I use select2 and knockoutJs with this simple binding:
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = ko.toJS(valueAccessor()) || {};
setTimeout(function () {
$(element).select2(options);
}, 0);
}
};
Markup:
<select class="select2" style="width:100%" data-bind="optionsCaption: '',options: $root.items,optionsText: 'description',optionsValue: 'id', value: $root.selectedItem,select2: { placeholder: 'Select an item...',allowClear: true }"></select>
it works! Now I enabled the allowClear
option in Select2 to clear dropdown to a placeholder value like Select an item...
.
If I click the x
icon dropdown properly sets the placeholder but knockout don't update observable binded value!
I think I've to change custombinding adding something like this:
setTimeout(function () {
$(element).select2(options).on("select2-removed", function (e) {
ko.bindingHandlers.value.update(element, function () { return ''; });
});
...
but it won't work!
There are couple of issues.
1) the update
in bindinghandler is to update DOM based on value change, you should never define an update
callback with the ability to mutate your model.
The right way is, when define a new bindinghandler, use init
callback to hook up all change
events with the model, the update
callback is merely a DOM drawing routine.
If your init
provided DOM drawing (such as provided by select2), you don't need to define an update
callback.
Hence the line ko.bindingHandlers.value.update(element, function () { return ''; });
only redraw the DOM, it doesn't do what you want.
2) the select2
binding you created has some holes.
- first,
value
binding doesn't know the existence of select2
binding, that's where you struggled.
- second, your
select2
binding has to wait for other binding (the options
binding) to finish DOM creation, what's why you use setTimeout
. But ko provided a way to define sequence of the bindings, just look the source code of ko value
binding, it's defined as 'after': ['options', 'foreach']
- third, your select2 doesn't respond to outside change. For instance, if you have another UI to update
$root.selectedItem
(a normal select list), the change raised by that UI would not sync back to your select2.
The solution
Define select2
binding based on existing value
binding (just found out it's not needed), and hook up all change events.
- we don't need
"select2-removed"
event, it's all about "change"
event.
select2
provided all drawing, we don't need update
callback.
- use a flag
shouldIgnore
to break the loop between value subscriber and select2 change event handler.
http://jsfiddle.net/huocp/8N3zX/6/
http://jsfiddle.net/huocp/8N3zX/9/
ko.bindingHandlers.valueSelect2 = {
'after': ['options'],
'init': function(element, valueAccessor, allBindings) {
// kind of extend value binding
// ko.bindingHandlers.value.init(element, valueAccessor, allBindings);
var options = allBindings.get('select2Options') || {};
$(element).select2(options);
var value = valueAccessor();
// init val
$(element).val(ko.unwrap(value)).trigger("change");
var changeListener;
if (ko.isObservable(value)) {
var shouldIgnore = false;
changeListener = value.subscribe(function(newVal) {
if (! shouldIgnore) {
shouldIgnore = true;
// this is just select2 syntax
$(element).val(newVal).trigger("change");
shouldIgnore = false;
}
});
// this demo only works on single select.
$(element).on("change", function(e) {
if (! shouldIgnore) {
shouldIgnore = true;
if (e.val == '') {
// select2 use empty string for unselected value
// it could cause problem when you really want '' as a valid option
value(undefined);
} else {
value(e.val);
}
shouldIgnore = false;
}
});
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (changeListener) changeListener.dispose();
$(element).select2("destory");
});
}
};