Knockout 3/Select2 update issue

by Doug Hill

HTML

<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.1.0/knockout-min.js"></script>
<script src="http://ivaynberg.github.io/select2/select2-3.4.8/select2.js"></script>
<p>Try changing "one" to something else and tabbing away; notice that the select2 below does not update its selected item because it's not triggering the update properly.</p>

<p>If you point your browser's element inspector, you'll notice that the background [select] element's options and stuff are getting changed, but this isn't causing an update in the select2 because the trigger("change") bit isn't firing.</p>

<p>This did work in Knockout 2.x, probably because Knockout 2 reevaluates all bindings every time one of them changes, and it re-triggers the options binding when the options text values change. However, evaluating the options binding in the select2 binding's update part doesn't seem to fix it.</p>

<div data-bind="foreach: choices">
    <div><input type="text" data-bind="value: choice" /></div>
</div>
<select data-bind="options: choices, optionsText: 'choice', optionsValue: 'choice', select2: {}"></select>

CSS

/*
Version: @@ver@@ Timestamp: @@timestamp@@
*/
.select2-container {
    margin: 0;
    position: relative;
    display: inline-block;
    /* inline-block for ie7 */
    zoom: 1;
    *display: inline;
    vertical-align: middle;
}

.select2-container,
.select2-drop,
.select2-search,
.select2-search input {
  /*
    Force border-box so that % widths fit the parent
    container without overlap because of margin/padding.
    More Info : http://www.quirksmode.org/css/box.html
  */
  -webkit-box-sizing: border-box; /* webkit */
     -moz-box-sizing: border-box; /* firefox */
          box-sizing: border-box; /* css3 */
}

.select2-container .select2-choice {
    display: block;
    height: 26px;
    padding: 0 0 0 8px;
    overflow: hidden;
    position: relative;

    border: 1px solid #aaa;
    white-space: nowrap;
    line-height: 26px;
    color: #444;
    text-decoration: none;

    border-radius: 4px;

    background-clip: padding-box;

    -webkit-touch-callout: none;
      -webkit-user-select: none;
         -moz-user-select: none;
          -ms-user-select: none;
              user-select: none;

    background-color: #fff;
    background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff));
    background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%);
    background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%);
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);
    background-image: linear-gradient(to top, #eee 0%, #fff 50%);
}

.select2-container.select2-drop-above .select2-choice {
    border-bottom-color: #aaa;

    border-radius: 0 0 4px 4px;

    background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff));
    background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%);
    background-image: -moz-linear-gradient(center bottom, #eee...

JavaScript

ko.bindingHandlers["select2"] = {
	after: ["options", "value", "selectedOptions"],
	init: function (element, valueAccessor) {
		$(element).select2(ko.unwrap(valueAccessor()));

		ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
			$(element).select2("destroy");
		});
	},
	update: function (element, valueAccessor, allBindingsAccessor) {
        //trying various methods to register in interest in updating this, but these work with the options observable, not the individual value observables
		//var allBindings = allBindingsAccessor();
		//if (allBindings.options) { allBindings.options(); }
		//if (allBindings.value) { allBindings.value(); }
		//if (allBindings.selectedOptions) { allBindings.selectedOptions(); }
		$(element).trigger("change");
	}
};
(function () {
	var updateOptions = ko.bindingHandlers["options"]["update"];
	ko.bindingHandlers["options"]["update"] = function (element) {
		var ret = updateOptions.apply(null, arguments);
		var $el = $(element);
		if ($el.data("select2")) { $el.trigger("change"); }
		return ret;
	}
})();
(function () {
	var updateSelectedOptions = ko.bindingHandlers["selectedOptions"]["update"];
	ko.bindingHandlers["selectedOptions"]["update"] = function (element) {
		var ret = updateSelectedOptions.apply(null, arguments);
		var $el = $(element);
		if ($el.data("select2")) { $el.trigger("change"); }
		return ret;
	}
})();


function Choice(choice) {
    this.choice = ko.observable(choice);
}

function ViewModel() {
    this.choices = ko.observableArray([
        new Choice("one"),
        new Choice("two"),
        new Choice("three")
    ]);
    this.myChoice = ko.observable();
}

var model = new ViewModel();
ko.applyBindings(model);