0
votes

So I have a a number of select2 dropdowns bound using Knockout. And initially it seemed to work fine. When a user makes selections(multi select). The model is updated and life is good. My problem comes in when you have values from the model that you want to displayed as selected by default. For example when editing something. You load the form with pre-populated values.

Here is my binding handler

ko.bindingHandlers.select2 = {
    init: function (el, valueAccessor, allBindingsAccessor) {
        ko.utils.domNodeDisposal.addDisposeCallback(el,
            function () {
                $(el).select2('destroy');
            });

        var allBindings = allBindingsAccessor(),
            select2 = ko.utils.unwrapObservable(allBindings.select2);

        $(el).select2(select2);
    },
    update: function (el, valueAccessor, allBindingsAccessor) {
        var allBindings = allBindingsAccessor();

        if ("value" in allBindings) {
            if ((allBindings.select2.multiple || el.multiple) && allBindings.value().constructor !== Array) {
                $(el).val(allBindings.value().split(',')).trigger('change');
            } else {
                $(el).val(allBindings.value()).trigger('change');
            }
        } else if ("selectedOptions" in allBindings) {
            var converted = [];
            var textAccessor = function (value) { return value; };
            if ("optionsText" in allBindings) {
                textAccessor = function (value) {
                    var valueAccessor = function (item) { return item; }
                    if ("optionsValue" in allBindings) {
                        valueAccessor = function (item) { return item[allBindings.optionsValue]; }
                    }
                    var items = $.grep(allBindings.options(), function (e) { return valueAccessor(e) === value });
                    if (items.length === 0 || items.length > 1) {
                        return "UNKNOWN";
                    }
                    return items[0][allBindings.optionsText];
                }
            }
            $.each(allBindings.selectedOptions(),
                function (key, value) {
                    converted.push({ id: value, text: textAccessor(value) });
                });
            $(el).select2("data", converted);
        }
        $(el).trigger("change");
    }
};

The selectedoptions is bound to a list of Guids. Which is a list of strings on the javascript side. As I believe the Select2 select requires strings to be able to set the selected values.

My select looks ike follows:

<select class="form-control input-sm multi-select" data-bind="selectedOptions: BrandIds, options: Brands,  valueAllowUnset: true, optionsText:'Description', optionsValue: 'Value', select2:{ placeholder: 'Please Select...', allowClear: true, multiple:'multiple'}, css: { 'jackpot-input-error': BrandIds.error() && BrandIds.isModified() }" multiple></select>

So what I find is happening is the inital valuesretruned from the server in BrandIds get removed. I determine this in the subscribe of the BrandIds. The parameters to the subscribe function look like this

[
{
    "status": "deleted",
    "value": "cd1f6c04-b7ae-479e-b722-65f837e65ec2",
    "index": 0
},
{
    "status": "deleted",
    "value": "bd66ebe1-080b-4455-9094-bf0464d4adbf",
    "index": 1
}

]

2
If you have not written this binding handler (and you haven't), please link to the source you have used, for reference and to give credit. - Tomalak

2 Answers

0
votes

I've removed the entire update portion of the binding handler, it seems pointless. Knockout will deal with the optionsText and optionsValue bindings.

Knockout manipulates the original <select> element, which is hidden in the background, and select2 notices these manipulations and updates its widget accordingly. There really does not seem to be any need to interfere.

Note how two options in the following sample are pre-selected from the viewmodel's initial state. When you change items in the BrandIds observable array through your own functions, e.g. after an Ajax request, the same thing should happen.

ko.bindingHandlers.select2 = {
    init: function (el, valueAccessor, allBindings) {
        var options = ko.unwrap(valueAccessor());

        $(el).select2(options);
        ko.utils.domNodeDisposal.addDisposeCallback(el, function () {
            $(el).select2('destroy');
        });
    }
};

var vm = {
  BrandIds: ko.observableArray(["ID_A", "ID_C"]),
  Brands: ko.observableArray([
    {Description: 'Brand A', Value: "ID_A"},
    {Description: 'Brand B', Value: "ID_B"},
    {Description: 'Brand C', Value: "ID_C"},
    {Description: 'Brand D', Value: "ID_D"}
  ])
};

ko.applyBindings(vm);
select {
    width: 300px;
}
pre {
    position: absolute;
    top: 10px;
    right: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.debug.js" integrity="sha512-per7WBYe3cT9aIDMoF74rYR7wpEDPqyncWqWzBGmJBnhp8H3ZD5fRdTM16IO5ePUEuBlH9DMKF7rHvuazhvDBA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js" integrity="sha512-2ImtlRlf2VVmiGZsjm9bEyhjGW4dU7B6TNwh/hx/iSByxNENtj3WVE6o/9Lj4TJeVXPi4bnOIMXFIJJAeufa0A==" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" integrity="sha512-nMNlpuaDPrqlEls3IX/Q56H36qvBASwb3ipuo3MxeWbsQB1881ox0cRv7UPTgBlriqoynt35KjEwgGUeUXIPnw==" crossorigin="anonymous" />

<select data-bind="
  selectedOptions: BrandIds,
  options: Brands,
  optionsText: 'Description',
  optionsValue: 'Value',
  select2: {
    placeholder: 'Please Select...',
    allowClear: true,
    multiple: 'multiple'
  }
" multiple></select>

<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
0
votes

So it turned out to be something not related to knockout or select2. We have a custom connection that we build up and send to the front end to bind to our selects. The string value here was an uppercase string representation of the guids. The BrandIds used to bind to the selectedOptions contained a list of guids that were all lower case. So it came down to the cases of the values being different.