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!

Be Sociable, Share!
Tagged: ,

Discussion

  1. Eyal says:

    Hey,

    Nice work. I also had to deal with this situation and I chose to comply with the way knockout meant it to be. My “selected” objects or their IDs are set when the app initializes and retrieves all the stuff it needs from the server. In my viewmodel, once the initialization callbacks are completed, I run a sync method. This sync method goes over all the objects that need to get synced and syncs them to their appropriate arrays, similar to what you did with this binding.

    Throughout the app’s lifecycle I will always use objects in existing arrays and will avoid getting into this situation as much as I can. If I can’t avoid it, I’ll run this sync method again on the specific array/object.

  2. Eduardo says:

    Your suggestion didn’t work as expected for me, but inspired me to create another one that solves this issue.

    I create a bindingHandler using init() and update() methods. On init(), I set the first value (that come from observable), and set change event to update the observables (associated to value and text).

    This handler will execute twice. At first time, select has no options bidden, so it will have no effect. But, at second time, with options bindden and created, every works fine.

    An example:


    ko.bindingHandlers.selectedValue = {
    init: function (element, valueAccessor) {
    //parse binding options
    var params = ko.utils.unwrapObservable(valueAccessor());

    //update option element according to the observable
    $(element).find('option[value="' + params.value() + '"]').prop('selected', true);

    //when select changes, update the observables
    $(element).change(function () {
    params.value($(element).find('option:selected').val());
    params.text($(element).find('option:selected').text());
    });
    },

    update: function (element, valueAccessor) {
    //parse binding params
    var params = ko.utils.unwrapObservable(valueAccessor());

    //when select changes, update the observable
    $(element).find('option[value="' + params.value() + '"]').prop('selected', true);
    }
    };

  3. Eduardo says:

    select data-bind=”options: $root.fontList, optionsText: ‘Alias’, selectedValue: { value : Font.FontID, text: Font.Alias}”>/select>

  4. Tania says:

    I also ran into this issue previously, and created my own workaround. I didn’t really want a workaround, so I was searching the web to see if I was just missing something, but I guess not. So, since we must use workarounds, I thought I would share mine. It is similar to the idea of “workaround 2” and Eduardo’s suggestion, but I used KO’s “optionsAfterRender” binding to directly link to the options array:

    Then:

    var linkToArray = function (option, data) {
    if (koObsVariable() && data) { //need to check for existence, jic
    if (koObsVariable().property== data.property) {
    koObsVariable(_.find(koObsArray(), function (v) {
    return v.property== koObsVariable().property;
    }))
    }
    }
    };

Add a Comment

*