Knockout.js and Select Options Binding Pre Selection

With the move into MVC4 I have taken an interest in knockout.js. Microsoft seemed fit to include it with MVC4 so it was worth taking a look at. To my surprise it will undoubtedly save me some time and effort for complex ajax UIs. I have already hit a major stumbling block though with it. That is the fact that when you bind an object to a select knockout does not work as you would expect. For example:

var Font = {FontID: 1, Alias: "Arial"}
 
var FontList = [/*An array of the structure above*/];
<select data-bind="options: $root.fontList, optionsText: 'Alias', value: Font"></select>

What you would expect is that no matter at what index the Font object was that it would select the proper item in the select menu when you loaded the page. This is not the case. Javascript does not consider two objects truly equal unless it is a reference to an exact object. This is why the menus don’t pre select the way you expect. Personally I think knockout.js needs to work the way people expect. That is, it is able to store objects as the values of a select, therefore it should have a mechanism to allow you to pre select one based on a pre determined key.

Workaround 1

Faced with this problem, one of the easiest ways around is to change the way you bind to the select. Doing below you will properly pre select the value you want. However, the only issue here is that when you update the value in the select you essentially corrupt your data. If say you have a Font object, the key would properly update, but the “Alias” value would not. Obviously this solution is not ideal.

<select data-bind="options: $root.fontList, optionsText: 'Alias', optionsValue: 'FontID', value: Font.FontID"></select>

Workaround 2

In my search for a solution many people suggested holding the selected value in a separate observable. You pre populate that observable with a reference to an object in the FontList array. This would work for a lot of people, but not for me. When I fetch my data from the server it is already nicely formatted and held in a structured object. If I have to break that structure for every drop down then the usefulness of knockout starts to come into question.

Final Solution

After toying with the idea of simply editing the knockout source code to make it work the way I expected I ended up finding out that making a custom binding handler would solve my issue. Here it is below along with the usage.

ko.bindingHandlers.preSelect = {       
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var val = ko.utils.unwrapObservable(valueAccessor());
        var newOptions = element.getElementsByTagName("option");
        var updateRequired = false;
        for (var i = 0, j = newOptions.length; i < j; i++) {
            if (ko.utils.unwrapObservable(val.value) == ko.selectExtensions.readValue(newOptions[i])[val.key]) {
                if (!newOptions[i].selected)
                {
                    ko.utils.setOptionNodeSelectionState(newOptions[i], true);//only sets the selectedindex, object still holds index 0 as selected
                    updateRequired = true;
                }
            }
        }
        if (updateRequired)
        {
            var options = allBindingsAccessor().options;
            var selected = ko.utils.arrayFirst(options, function (item) {
                return ko.utils.unwrapObservable(val.value) == item[val.key];
            });
            if (ko.isObservable(bindingContext.$data[val.propertyName])) {
                bindingContext.$data[val.propertyName](selected); // here we write the correct object back into the $data
            } else {
                bindingContext.$data[val.propertyName] = selected; // here we write the correct object back into the $data
            }
        }
    }
};
<select data-bind="options: $root.fontList, optionsText: 'Alias', value: Font, preSelect: {key : 'FontID', propertyName : 'Font', value : Font.FontID}"></select>

In my opinion this is not an idea solution because of all the data I had to pass back in the preSelect argument. In my particular situation because of the way my objects are structured I had to know all three parameters. You situation may be different so adjust the code accordingly. I found that even though you set the selected index using knockouts method it does not update the referencing object, so the last line is there to do that. Overall this solution solves my issue and I haven’t found any pitfalls yet, but if you do let me know!

Tagged: ,