1

I would like some help on Select /Select2 functionality for a "combo" box that I am trying to set up in .Net with knockout/ jquery. I need to preload this "combo" box with an array of values, then allow the user to add additional (select from the dropdown or type in something else) or delete entries. The below code works as it should as long as the values that are being preloaded to the "combo" box exist in the coordinating dropdown (allusers). If the preloaded values do not exist in the dropdown, they are simply bypassed.

Has anyone else had to deal with this issue? How did you overcome it? I would appreciate any feedback or ideas out there!

<select data-bind="selectedOptions: newMessageTo, optionsCaption: 'Select a user...', 
    options: allUsers, optionsText: 'userName', optionsValue: 'userName', 
    select2: { placeholder: 'Select a user...', allowClear: true, multiple: true, 
    minimumInputLength: 3, tags: true, minimumResultsForSearch: 20 }">
</select>
self.myMessage = ko.observable({});
self.myMessageText = ko.observable("");
self.messageId = ko.observable({});
self.newMessageText = ko.observable("");
self.newMessageTo = ko.observableArray([]);
self.newMessageSubject = ko.observable("");
self.newAttachment = ko.observable({});
self.newAttachmentList = ko.observableArray([]);
self.allUsers = ko.observable({});
self.bearer = ko.observable();
self.newStyleAttachments = ko.observable(false);

self.replyAll = function () {
    self.replying() ? self.replying(false) : self.replying(true);
    self.newMessageTo.removeAll();
    for (var i = 0; i < self.myMessage().cc.length; i++) {
        if (self.myMessage().cc[i] !== self.user.email) {
            self.newMessageTo.push(self.myMessage().cc[i]);
        }
    }
    self.newMessageTo.push(self.myMessage().senderEmail);
    self.newMessageText("<br/><center>-------------------------------------------</center><br/> \
        <b>From:</b> " + self.myMessage().senderEmail + " <br/><b>To:</b> " + self.myMessage().cc + " \
        <br/><b>Subject:</b> " + self.myMessage().messageSubject + "<br/>" + self.myMessageText());
    self.newMessageSubject("RE: " + self.myMessage().messageSubject);
};
ko.bindingHandlers.select2 = {
    init: function (el, valueAccessor, allBindingsAccessor, viewModel) {
        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, viewModel) {
        var allBindings = allBindingsAccessor();

        if ("value" in allBindings) {
            if ((allBindings.select2.multiple || el.multiple) && allBindings.value().constructor != Array) {
                $(el).val(allBindings.value().split(',')).change();
            }
            else {
                $(el).val(allBindings.value()).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).change();
    }
};
RebeccaS
  • 13
  • 5
  • *then allow the user to **add additional (select from the dropdown** or type in something else) or delete entries* - let's focus on the bolded part: as I understand, you've got 2 dropdowns: one whose html you provided here (using select2); and another one that have some `onChange` handler, that takes its selected value and add them to the first one. can you add that code to your question? – OfirD Jan 21 '20 at 13:40
  • Hi there @HeyJude and thank you for your response. I am using only one dropdown but the selections are pre-populated when the screen comes up. The user can delete the pre-populated selection(s) and / or add new values either by selecting something from the dropdown, or typing in something that is NOT in the dropdown. The problem that is occurring is that when the select box is populated from an array of values, and if one of those values do not exist in the dropdown, then that value is not being displayed in the select box. I hope this makes sense :-) – RebeccaS Jan 21 '20 at 18:14
  • I'm not sure what "selections" are, so let's be more precise and only use the terms "options" and "values". So, correct me if I'm wrong again: you've got a dropdown, pre-populated with multiple *options* and multiple *values*, and the user, or code, can add more *values* to it by selecting from existing *options*, or typing something into it (in which case, both option and value are added). Now, some code adds *values* **which aren't present as existing *options*** to your dropdown, and these *values* are not displayed. Did I got you right? – OfirD Jan 21 '20 at 18:33
  • @HeyJude, Yes I think you got it! – RebeccaS Jan 21 '20 at 18:53
  • Great, I edited my answer to match your use case. Compare the code to yours and see where it went wrong. – OfirD Jan 21 '20 at 18:54
  • basically i build an array with all of the values that are displayed in the select box. this array can contain values from the options shown, or new values as typed by the user. i can type new values into the select and they are displayed and saved properly. it is only when i initially populate values to the select box when the page is loaded, so if one of the values being loaded does not exist in the options, it is not being populated in the select box , and not getting saved to the array. – RebeccaS Jan 21 '20 at 18:56
  • Thank you @HeyJude!! You are awesome, I will take a look and see if this helps me pinpoint the issue and how to resolve it!! – RebeccaS Jan 21 '20 at 18:57
  • Your welcomed. don't forget to mark my answer as accepted if it solves your problem, and let me know if it doesn't. – OfirD Jan 21 '20 at 18:59

1 Answers1

0

My guess is that it's something to do with your select2 knockout binding (which is not present in the question).

The following is a working example with quite a simple binding handler, taken from here. Simply run the code snippet, which pre-populate a dropdown with 2 options and 1 value, then (after 5 seconds) adds 2 values to it - one which exists as an option, and one which isn't (and is therefore also added as a new option):

$(document).ready(function () {
  registerBinding();
  run();
});

var vm = function () {
  var self = this;
  self.available = ko.observableArray(['John', 'Joe']);
  self.selected = ko.observableArray(['John']);
  self.add = function (otherUsers) {
    otherUsers.forEach(function (otherUser) {
        var matchedAvailable = self.match(self.available(), otherUser);
        var matchedSelected = self.match(self.selected(), otherUser);
        if (!matchedAvailable) {
            self.available.push(otherUser);
        }
        if (!matchedSelected) {
            self.selected.push(otherUser);
        }
    });
  };
  self.match = function (users, userToMatch) {
      var matched = ko.utils.arrayFirst(users, function (user) {
          return user == userToMatch;
      });
      return matched;
  };
};

function run() {
  var vmInstance = new vm();
  ko.applyBindings(vmInstance);
  setTimeout(function () {
    vmInstance.add(['Jane', 'Joe']);
  }, 5000);
}

function registerBinding() {
  ko.bindingHandlers.select2 = {
      after: ["options", "value"],
      init: function (el, valueAccessor, allBindingsAccessor, viewModel) {
          $(el).select2(ko.unwrap(valueAccessor()));
          ko.utils.domNodeDisposal.addDisposeCallback(el, function () {
              $(el).select2('destroy');
          });
      },
      update: function (el, valueAccessor, allBindingsAccessor, viewModel) {
          var allBindings = allBindingsAccessor();
          var select2 = $(el).data("select2");
          if ("value" in allBindings) {
              var newValue = "" + ko.unwrap(allBindings.value);
              if ((allBindings.select2.multiple || el.multiple) && newValue.constructor !== Array) {
                  select2.val([newValue.split(",")]);
              }
              else {
                  select2.val([newValue]);
              }
          }
      }
  }
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.12/js/select2.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.12/css/select2.min.css" />

<div class="col-xs-4">
    <label for="users">Users</label>
    <select id="users" class="form-control"
            multiple="multiple"
            data-bind="options: available,
                selectedOptions: selected,
                select2: { placeholder: 'Select an option...', allowClear: true }">
    </select>
</div>
OfirD
  • 9,442
  • 5
  • 47
  • 90
  • I am still not getting the select2 to work! The issue is that the select box Options is bound to an array that contains all of the ids and values that are available in the dropdown. The SelectedOptions is bound to an array that contains a list of values that may not exist in SelectedOptions. The values in SelectedOptions are NOT displayed in the select box if the selectedOption is not in the Options array – RebeccaS Jan 22 '20 at 21:01
  • The array bound to `options` is the only one whose items appear as options in the dropdown. So if you add an item into the array bound to `selectedOptions` without adding this item into the array bound to `options` *first*, it won't be displayed. – OfirD Jan 22 '20 at 21:10