Search code examples
knockout.jsjquery-select2jquery-select2-4

Select2 with ajax, multiple and knockout binding not saving objects to selectedOptions


I am evaluating select2 v4.0 in relation to us upgrading from v3.5.x. we are using this with knockout v3.5.2. Our scenario is that we want to allow for user to select multiple options based on js objects coming from an ajax call. the selectedoptions binding should store the entire js object in the observablearray its bound to and not just the list of ids.

in my testing, here is what I discovered

This is a select2 with a predefined list of options. each option has a id, text and displayText property. the result is that the multipleSelectedObjects observable array ends up having an array of the entire js object which is what I would expect based on the fact that I didn't set an optionsValue argument

<select class="multipleSelect" data-bind="options: multipleOptions, selectedOptions: multipleSelectedObjects, optionsText: 'displayText', select2v4: multipleOptionsSetup" style="width: 200px">

This is a select2 with a predefined list of options. each option has a id, text and displayText property. the result is that the multipleSelectedValue observable array ends up having an array of ints representing the id of the js objects selected which is what I would expect based on the fact that I set the optionsValue argument

<select class="multipleSelect3" data-bind="options: multipleOptions, selectedOptions: multipleSelectedValue, optionsText: 'text', optionsValue: 'id', select2v4: multipleOptionsSetup" style="width: 200px">

This is a select2 that is using the ajax option to make a service call to return options. each option returned has a id, text and displayText property. the result is that the selectedItems observable array ends up having an array of ints representing the id of the js objects selected. This is not what I would expect. I tried setting just the optionsText property as I did with the first select2 above, but that didn't make a difference

<select id="select2Input" data-bind="selectedOptions: selectedItems, select2v4: selectSetup" style="width: 400px">

I think this might be a select2 issue, but im not sure. Our select2v4 binding handler is shown below. in the last scenario when knockout hits the selectedOptions section, the value is a string which makes me think select2 is not setup to pass the entire object when using ajax. anyone else experience this? is this a bug or by design?

// NOTE: this binding handler is made for select2 version 4.0

 ko.bindingHandlers.select2v4 = { 

     init: function (element, valueAccessor, allBindingsAccessor) { 

         var bindingValue = ko.unwrap(valueAccessor()); 

         var allBindings = allBindingsAccessor(); 

         var valueDataChange; 

         // Observe external data changes; set data on change 

         if (ko.isObservable(allBindings.valueData)) { // special data binding 

             var onChange = false; 

             allBindings.valueData.subscribe(function (value) { // subscribe to external data changes 
                 if (onChange) return; // ignore if on change to prevent recursion 
                 $(element).select2("data", value, false); // set data explicitly; do not trigger change }); 

             if (ko.isWritableObservable(allBindings.valueData)) { 

                 valueDataChange = function () { 

                     onChange = true; // suppress valueData subscription 

                     allBindings.valueData($(element).select2("data")); 

                     onChange = false; 

                 }; 

                 $(element).on("change", valueDataChange); // update observable on data change 

             } 

         } 

         // Observe external value changes 

         else if (ko.isObservable(allBindings.value)) { // input or single select with observable value binding 

             allBindings.value.subscribe(function (value) { // subscribe to external value changes 

                 if (typeof value === "string") { // optionsValue or tags specified 

                     if (bindingValue.tags) { // tags specified 

                         value = value.split(bindingValue.separator || ','); // split on value separator 

                     } 

                     $(element).val(value); // set val to allow select2 to resolve data from values; do not trigger change 

                 } 
                 else { // optionsValue not specified, value is complex data 

                     $(element).select2("data", value, false); // set data explicitly; do not trigger change 

                 } 

             }); 

         } 

 // Observe external selection changes 

         else if (ko.isObservable(allBindings.selectedOptions)) { // multiselect with observable selection binding 

             allBindings.selectedOptions.subscribe(function (value) { // subscribe to external selection changes 

                 if (value.length > 0 && typeof value[0] === "string") { // optionsValue specified 

                     $(element).val(value); // set val to allow select2 to resolve data from values; do not trigger change 

                 } 
                 else { // optionsValue not specified, value is complex data 

                     $(element).select2("data", value, false); // set data explicitly (only works if complex data object has 'id' property); do not trigger change 

                 } 

             }); 

         } 

         // Destroy select2 on element disposal 

         ko.utils.domNodeDisposal.addDisposeCallback(element, function () { 

             $(element).select2('destroy'); 

             if (valueDataChange != null) { 

                 $(element).off("change", valueDataChange); 

             } 

         }); 

         // Apply select2 and initialize data; delay to allow other binding handlers to run 

         setTimeout(function () { 

             // Apply select2 

             $(element).select2(bindingValue); 

             // Initialize data 

             if ("valueData" in allBindings) { 

                 $(element).select2("data", ko.unwrap(allBindings.valueData), false); 

             } 

         }, 0); 

      } 

  };

Solution

  • There were a number of big changes in how select2 v4 works internally compared to prior versions. One of them is how select2 is storing selected objects in the case of using ajax to load data (aka typeahead). A coworker of mine was able to figure out that in that case, the selected objects are stored in a readonly collection and do not properly notify when changes are made, plus the normal binding doesn't work as expected. As we discovered, binding to selectedOptions did not result in a list of objects, but of ids. He came up with the binding handler for select2 shown below that will deal with both regular select style binding and adds a new binding called selectData, which will get you a list of selected objects if you are using remote data loading with ajax. You only need the selectData binding if you are using ajax to load data and want to get a list of the selected objects instead of ids, otherwise you can either bind to value or selectedOptions and types of setup will work as expected.

    <select id="select2Input" data-bind="select2Data: selectedItems, select2: selectSetup" style="width: 400px"></select>
    

    https://github.com/shaftware/knockout-select2