title: Knockout.js custom binding with bootstrap.multiselect.js not updating selected value
body: <p>I am using Knockout in combination with <a href="http://davidstutz.github.io/bootstrap-multiselect/" rel="nofollow">bootstrap-multiselect</a>. I have three columns in an HTML table which allow you to specify a search item, search operator and search criteria. The search operator column actually holds three different elements: one multi-select, one single select and an input element; only one out of the three elements is actually displayed at any given time. Which element is displayed is controlled by the value of the search item column.  </p>
  
  <p>All select elements are initialized as bootstrap-multiselect. The search criteria column and the search operator column work without issue.  The search item column however does not seem to be properly binding back to the native select which bootstrap-multiselect hides.  The search item column will have the radio button selected next to the value you picked, however the text will display the first value from the select list.  If I do not initialize the search item column as a bootstrap-multiselect, everything works as expected with the native select for the search-item column.  The Knockout viewmodel is updated with the correct value as expected.</p>
  
  <p>Below is my HTML declaration for the search criteria table:</p>
  
  <pre><code>&lt;table id="search-criteria" class="table table-condensed hidden" data-bind="css: {hidden:SpecifiedCriteria().length == 0}"&gt;
      &lt;thead&gt;
      &lt;tr&gt;
          &lt;th class ="search-item"&gt;
              Item
          &lt;/th&gt;
          &lt;th class="search-operator"&gt;
              Operator
          &lt;/th&gt;
          &lt;th class="search-criteria"&gt;
              Criteria
          &lt;/th&gt;
      &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody data-bind="foreach:SpecifiedCriteria"&gt;
      &lt;tr&gt;
          &lt;td class="search-item"&gt;
              &lt;select class="search-item" data-bind="searchDropDownList:SelectedItem, foreach: $root.CriteriaItems, value:SelectedItem"&gt;
                  &lt;optgroup data-bind="attr: {label: label}, foreach: children"&gt;
                      &lt;option data-bind="text: DisplayName, option: $data"&gt;&lt;/option&gt;
                  &lt;/optgroup&gt;
              &lt;/select&gt;
          &lt;/td&gt;
          &lt;td class="search-operator"&gt;
              &lt;select class="search-operator" data-bind="searchDropDownList:MatchCompareOperator, options:AvailableCompareOperators(), optionsValue: 'Value', optionsText: 'Key', value:MatchCompareOperator"&gt;&lt;/select&gt;
          &lt;/td&gt;
          &lt;td class="search-criteria"&gt;
              &lt;input type="text" class="search-criteria" data-bind="value:MatchValue" /&gt;
              &lt;select class="search-criteria" data-bind="searchDropDownList:MatchValueListSelectedItemIds, options:MatchValueList, optionsValue:'Id', optionsText:'Description', selectedOptions:MatchValueListSelectedItemIds"&gt;&lt;/select&gt;
              &lt;select class="search-criteria" multiple="multiple" data-bind="searchDropDownList:MatchValueListSelectedItemIds, options:MatchValueList, optionsValue:'Id', optionsText:'Description', selectedOptions:MatchValueListSelectedItemIds"&gt;&lt;/select&gt;
          &lt;/td&gt;
      &lt;/tr&gt;
      &lt;/tbody&gt;
  &lt;/table&gt;
  </code></pre>
  
  <p>This is my view model, of which I new up an instance and pass into ko.applyBindings():</p>
  
  <pre><code>searchCriteria = function () {
  
      var comparisonOperators = Array(
          { "Key": "Starts With", "Value": 0 },
          { "Key": "Greater Than", "Value": 1 },
          { "Key": "Less Than", "Value": 2 },
          { "Key": "Greater Or Equal To", "Value": 3 },
          { "Key": "Less or Equal To", "Value": 4 },
          { "Key": "Not Equal To", "Value": 5 },
          { "Key": "Equal To", "Value": 6 },
          { "Key": "Contains", "Value": 7 },
          { "Key": "Does Not Contain", "Value": 8 });
  
      var criteriaDataType = {
          Alpha: 0,
          AlphaNumeric: 1,
          Integer: 2,
          Date: 3,
          DateTime: 4,
          Boolean: 5
      };
  
      //available styles constants
      var matchStyle = {
          InputBox: 0,
          DropDownList: 1,
          MultiSelectDropDownList: 2,
          DatePicker: 3
      };
  
      //available operator constants
      var matchCompareOperator = {
          StartsWith: 0,
          GreaterThan: 1,
          LessThan: 2,
          GreaterOrEqualTo: 3,
          LessOrEqualTo: 4,
          NotEqualTo: 5,
          EqualTo: 6,
          Contains: 7,
          DoesNotContain: 8
      };
  
      //determine which if the operator is eligible for the matchStyle
      function filterOperators(operator, specifiedMatchStyle, specifiedCriteriaDataType) {
          var eligible = false;
  
          switch (specifiedMatchStyle) {
              case matchStyle.MultiSelectDropDownList:
              case matchStyle.DropDownList:
                  eligible = (operator.Value === matchCompareOperator.EqualTo || operator.Value === matchCompareOperator.NotEqualTo);
                  break;
              case matchStyle.DatePicker:
                  eligible = (operator.Value !== matchCompareOperator.StartsWith &amp;&amp;
                                     operator.Value !== matchCompareOperator.Contains &amp;&amp;
                                     operator.Value !== matchCompareOperator.DoesNotContain);
                  break;
              case matchStyle.InputBox:
                  if (specifiedCriteriaDataType !== criteriaDataType.Integer &amp;&amp;
                      specifiedCriteriaDataType !== criteriaDataType.Date &amp;&amp;
                      specifiedCriteriaDataType !== criteriaDataType.DateTime) {
  
                      if (operator.Value !== matchCompareOperator.GreaterThan &amp;&amp;
                          operator.Value !== matchCompareOperator.GreaterOrEqualTo &amp;&amp;
                          operator.Value !== matchCompareOperator.LessThan &amp;&amp;
                          operator.Value !== matchCompareOperator.LessOrEqualTo) {
  
                          eligible = true;
                      }
  
                  } else {
                      if (operator.Value !== matchCompareOperator.StartsWith)
                          eligible = true;
                  }
  
                  break;
          }
  
          return eligible;
      }
  
      var searchCriteriaModel = function (criteriaItems, postUrl) {
  
          var modelObject = this;
  
          //setup the model elements
          modelObject.SpecifiedCriteria = ko.mapping.fromJS([]);
          modelObject.ComparisonOperators = comparisonOperators;
  
          var resultsDataTable = $("#search-results").dataTable();
  
          //create the criteria group items
          modelObject.CriteriaItems = [];
  
          var group = function (label, children) {
              this.label = ko.observable(label);
              this.children = ko.observableArray(children);
          }
  
          var groups = ko.utils.arrayMap(criteriaItems, function (item) { return item.DisplayGroupName; });
          groups = ko.utils.arrayGetDistinctValues(groups).sort();
  
          //filter the criteria items array based on the group and build the list of grouped items
          ko.utils.arrayForEach(groups, function (groupItemDisplayGroupName) {
              modelObject.CriteriaItems.push(
                  new group(groupItemDisplayGroupName,
                      ko.utils.arrayFilter(criteriaItems, function (item) {
                          return (item.DisplayGroupName === groupItemDisplayGroupName);
                      })
                  )
              );
          });
  
          var criteriaItem = function () {
              var itemObject = this;
  
              //setup the properties we are binding observables on
              itemObject.SelectedItem = ko.observable();
              itemObject.MatchValueColumnName = ko.observable();
              itemObject.MatchCompareOperator = ko.observable();
              itemObject.MatchValue = ko.observable();
              itemObject.DisplayName = ko.observable();
              itemObject.MatchStyle = ko.observable();
              itemObject.MatchValueList = ko.observableArray();
              itemObject.MatchValueListSelectedItemIds = ko.observableArray();
              itemObject.AvailableCompareOperators = ko.observableArray();
  
              //a computed which contains just the list of our selected items
              itemObject.MatchValueListSelectedItems = ko.computed(function () {
                  return ko.utils.arrayFilter(itemObject.MatchValueList(), function (matchValueListItem) {
                      return ko.utils.arrayFirst(itemObject.MatchValueListSelectedItemIds(), function (selectedItemId) {
                          return (matchValueListItem.Id === selectedItemId);
                      });
                  });
              });
  
              itemObject.SelectedItem.subscribe(function (newValue) {
                  if (newValue === undefined || newValue === null)
                      return;
  
                  //copy over all of the other attributes from the selected item to our item
                  itemObject.DataIdColumnName = newValue.DataIdColumnName;
                  itemObject.DataIdColumnValue = newValue.DataIdColumnValue;
                  itemObject.DisplayGroupName = newValue.DisplayGroupName;
                  itemObject.DisplayName(newValue.DisplayName);
  
                  itemObject.MatchValueDataType = newValue.MatchValueDataType;
                  itemObject.MatchValue(newValue.MatchValue);
                  itemObject.MatchStyle(newValue.MatchStyle);
                  itemObject.MatchValueColumnName = newValue.MatchValueColumnName;
  
                  //initialize the compareoperator list
                  itemObject.AvailableCompareOperators.removeAll();
                  ko.utils.arrayPushAll(itemObject.AvailableCompareOperators,
                      ko.utils.arrayFilter(modelObject.ComparisonOperators, function (operator) { return filterOperators(operator, newValue.MatchStyle, newValue.MatchValueDataType) }));
  
                  itemObject.MatchCompareOperator(newValue.MatchCompareOperator);
  
                  //initialize the value list
                  itemObject.MatchValueList.removeAll();
                  if (newValue.MatchValueList !== null)
                      ko.utils.arrayPushAll(itemObject.MatchValueList, newValue.MatchValueList);
  
                  itemObject.MatchValueListDisplayMember = newValue.MatchValueListDisplayMember;
                  itemObject.MatchValueListSelectedItemIds.removeAll();
              });
  
          };
  
          //add a new item
          modelObject.addCriteriaItem = function () {
              var item = new criteriaItem();
              modelObject.SpecifiedCriteria.push(item);
          };
  
          //clear all items
          modelObject.clearCriteriaItems = function () {
              modelObject.SpecifiedCriteria.removeAll();
              $("#item-criteria-results").addClass("hidden");
              resultsDataTable.fnClearTable();
              modelObject.addCriteriaItem();
          };
  
          //seed with an empty row
          modelObject.addCriteriaItem();
  
          ko.bindingHandlers.option = {
              update: function (element, valueAccessor) {
                  var value = ko.utils.unwrapObservable(valueAccessor());
                  ko.selectExtensions.writeValue(element, value);
              }
          };
  
          //set up the ko search-multiselect binding for use with bootstrap multiselect
          ko.bindingHandlers.searchDropDownList = {
              update: function (element, valueAccessor, allBindings, viewMode, bindingContext) {
  
                  if (element.nodeName.toLowerCase() !== "select")
                      return;
  
                  //get the value and wire up the subscription
                  var value = ko.utils.unwrapObservable(valueAccessor());
                  if (value === null || value === undefined)
                      return;
  
                  //cache the element so we don't have to look it up every time
                  var triggerElement = $(element);
                  var row = $(triggerElement).parents("tr");
  
                  //initialize the multi-select list
                  var searchItem = null;
                  var operator = $(row).find("select.search-operator");
  
                  //initialize the multi-selects if this is our first time through
                  if (!triggerElement.hasClass("initialized")) {
                      if (triggerElement.hasClass("search-item")) {
                          //make sure we deselect any item
                          //triggerElement.prop("selectedIndex", -1);
  
                          triggerElement.multiselect({
                              buttonWidth: "100%",
                              maxHeight: 310,
                              enableFiltering: true,
                              enableCaseInsensitiveFiltering: true
                          });
  
                      } else {
                          triggerElement.multiselect({ buttonWidth: "100%" });
                      }
  
                      //flag the element as initialized
                      triggerElement.addClass("initialized");
  
                      //toggle off the operator and all of the criteria entry boxes
                      operator.siblings(".btn-group").addClass("hidden");
                      $(row).find("select.search-criteria").siblings(".btn-group").addClass("hidden");
                      $(row).find("input.search-criteria").addClass("hidden");
                      return;
                  }
  
                  //toggle the display of the operator and the search criteria
                  if (triggerElement.hasClass("search-item")) {
  
                      if (operator.hasClass("initialized")) {
                          operator.multiselect("rebuild");
                          $(operator).siblings(".btn-group").first().removeClass("hidden");
                      }
  
                      //toggle off all of the criteria entry boxes
                      $(row).find("select.search-criteria").siblings(".btn-group").addClass("hidden");
                      $(row).find("input.search-criteria").addClass("hidden");
  
                      var searchCriteria = null;
                      if (value.MatchStyle === matchStyle.MultiSelectDropDownList)
                          searchCriteria = $(row).find("select[multiple='multiple'].search-criteria");
                      else if (value.MatchStyle === matchStyle.DropDownList)
                          searchCriteria = $(row).find("select:not([multiple]).search-criteria");
                      else
                          searchCriteria = $(row).find("input[type=text].search-criteria");
  
                      if (searchCriteria != null) {
                          if (searchCriteria.hasClass("initialized")) {
                              //we are a select
                              searchCriteria.multiselect("rebuild");
                              searchCriteria.next(".btn-group").removeClass("hidden");
                          } else {
                              searchCriteria.removeClass("hidden"); //we are an inputbox
                          }
                      }
                  }
              }
  
          };
      }
  
      return {
          criteriaDataType: criteriaDataType,
          matchStyle: matchStyle,
          matchCompareOperator: matchCompareOperator,
          SearchCriteriaModel: searchCriteriaModel
      }
  }();
  </code></pre>
  
  <p>Just to recap: if I do not wire up the <code>search-item</code> column as a bootstrap-multiselect, everything works.  If I do wire it up, the column will display the first value in the list.  Setting a breakpoint in the <code>searchDropDownList</code> binding handler will always show the value gotten via the valueAccessor to be the first item in the list for the given <code>search-item</code> element.  </p>
  
  <p>I am wits end and cannot understand or explain this behavior for the <code>searc-item</code> column, especially since the other two columns seeming work without issue.</p>