Bootstrap-multiselect: KnockoutJS binding to object value (not a simple value) ... Hack fix included

Created on 2 May 2015  ·  3Comments  ·  Source: davidstutz/bootstrap-multiselect

I am using the knockout select binding where an entire object is bound as the value...not just a string.

bootstrap-multiselect did not like this because it uses the "value" to relate its dropdown list items back to the corresponding select option. In order for it to do this you have to have set the "optionsValue" attribute in your knockout binding so that the option has a "value" attribute. However, setting the "optionsValue" attribute then has the expected result of setting only that value in my selectedOptions observableArray...not the entire object.

Its quite possible that I am the only person that uses the functionality within knockout that lets you bind the options to entire objects, but just in case I'm not...here's the hack fix:

Created a two new bootstrap-multiselect options called "optionsKey" and "observableKey" so that it knows where to go for a value that it can relate to:

<select id="mySelect" data-bind="options: myOptions, selectedOptions: mySelectedOptions, optionsText: 'myDisplayText', optionsAfterRender: SetOptionKey, multiselect: { optionsKey: 'data-key', observableKey: 'Key' }" multiple="multiple"></select>

In my knockout view model I set the attribute referenced by "optionsKey" on each option:

myVM.SetOptionKey = function ( option, item ) {
    ko.applyBindingsToNode( option, { attr: { "data-key": item.Key } }, item );
};

Now I dive into the bootstrap-multiselect.js file. The first thing I had to do in here is at the top where it is adding the knockout init handler.

init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
    ...
    if (allBindings.has('selectedOptions')) {
        var selectedOptions = allBindings.get('selectedOptions');
        if (ko.isObservable(selectedOptions)) {
            ko.computed({
                read: function () {
                    selectedOptions();

                    // Added to handle knockout binding to an object...not just a value
                    // multiselect needs the selected property on the options, else it wont show them on refresh
                    if ((config.optionsKey !== undefined) &&
                        (config.optionsKey !== null) &&
                        (config.optionsKey.length > 0) &&
                        (config.observableKey !== undefined) &&
                        (config.observableKey !== null) &&
                        (config.observableKey.length > 0)) {
                        var _optionsKey = config.optionsKey;
                        var _observableKey = config.observableKey;
                        ko.utils.arrayForEach(selectedOptions(), function (selectedOption) {
                            $("option[" + _optionsKey + "='" + selectedOption[_observableKey]() + "']", $element).prop('selected', true);
                        })
                    }

                    setTimeout(function () {
                        $element.multiselect('refresh');
                    }, 1);
                },
                disposeWhenNodeIsRemoved: element
            }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
        }
    }
    ...

Next I had to modify the createOptionValue function in bootstrap-multiselect.js to first use this key as the value it sets in its list items:

createOptionValue: function (element) {
    ...

    var value = $element.val();
    // Added to handle knockout binding to an object...not just a value
    if ((this.options.optionsKey !== undefined) && (this.options.optionsKey !== null) && (this.options.optionsKey.length > 0)) {
        var key = $element.attr(this.options.optionsKey);
        if ((key !== undefined) && (key !== null) && (key.length > 0)) {
            value = key;
        }
    }

    ...
}

Lastly I had to modify getOptionByValue function in bootstrap-multiselect.js to find option elements based on this attribute instead of value:

getOptionByValue: function (value) {
    var valueToCompare = value.toString();

    // Added to handle knockout binding to an object...not just a value
    if ((this.options.optionsKey !== undefined) && (this.options.optionsKey !== null) && (this.options.optionsKey.length > 0)) {
        var opt = $("option[" + this.options.optionsKey + "='" + valueToCompare + "']", this.$select).first();
        if( (opt !== undefined) && (opt !== null) ) {
            return $(opt);
        }
    }

    var options = $('option', this.$select);

    for (var i = 0; i < options.length; i = i + 1) {
        var option = options[i];
        if (option.value === valueToCompare) {
            return $(option);
        }
    }
},
question

All 3 comments

Thanks, will include this in the FAQ! I am sure this is helpful for other users, as well.

Thank you for this tip - I was trying to accomplish binding to select objects as well and this write up was very helpful. The only thing I had to do in addition to @APM3 's code is also populate the "value" attribute of my options in the SetOptionKey function:

self.SetOptionKey = function (option, item) { ko.applyBindingsToNode(option, { attr: { "data-key": item.Value } }, item); ko.applyBindingsToNode(option, { attr: { "value": item.Value } }, item); }

Thanks again for this write up.

See gist for knockout binding that supported object. Just replace the actual binding from bootstrap-multiselect.js file. In this code you dont need to put into your vm to set option.

//Just like this one
myVM.SetOptionKey = function ( option, item ) {
    ko.applyBindingsToNode( option, { attr: { "data-key": item.Key } }, item );
};

I added preprocess to bindings to implicitly addoptionsAfterRender.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sourav-prescriber-360 picture sourav-prescriber-360  ·  8Comments

Furgas picture Furgas  ·  8Comments

zephyx picture zephyx  ·  7Comments

rodrigonunes100 picture rodrigonunes100  ·  3Comments

mirroras picture mirroras  ·  3Comments