Question

The view code:

<ul data-bind="foreach: BackColorOptions">
    <li data-bind="css: { selected: Selected }">
        <label>
            <input type="radio" name="BackColorOption" 
                data-bind="value: Color, checked: $root.BackColor" />
        </label>
    </li>
</ul>
@{
    var jsonModel = new System.Web.Script.Serialization.
        JavaScriptSerializer().Serialize(Model);
}
<input type="hidden" id="JsonModel" value='@jsonModel' />

The viewmodel code:

var initialData = $.parseJSON($('#JsonModel').val());

function BackColorOption(data, parent) {
    var self = this;
    self.parent = parent;
    self.Text = ko.observable(data.Text);
    self.Color = ko.computed(function () {
        return '#' + self.Text().toLowerCase();
    });
    self.Selected = ko.computed(function () {
        var backColor = self.parent.BackColor();
        if (backColor) {
            return backColor.toLowerCase() == self.Color;
        }
        return false;
    });
}

function TestViewModel() {
    var self = this;

    self.BackColor = ko.observable(initialData.BackColor);

    var mappedBackColorOptions = $.map(initialData.BackColorOptions, 
        function (item) {
            return new BackColorOption(item, self);
        }
    );
    self.BackColorOptions = ko.observableArray(mappedBackColorOptions);

}

ko.applyBindings(new TestViewModel());

The model code:

string BackColor { get; set; }
SelectListItem[] BackColorOptions
{
    get
    {
        return new[] 
        { 
            new SelectListItem{Text = "cc0000"},
            new SelectListItem{Text = "ff9900"},
            new SelectListItem{Text = "dddd33"},
            new SelectListItem{Text = "009900"},
            new SelectListItem{Text = "00cccc"},
            new SelectListItem{Text = "0066ff"},
            new SelectListItem{Text = "9900ff"},
            new SelectListItem{Text = "ff00ff"},
        };
    }
}

The code above works as expected in IE (8) and Chrome (17), but not FF (10.0.2). I'm basically trying to do a color selector similar to GitHub's issue labels. The view renders a set of radio buttons you can click on to choose a color. When a radio is checked, I add a selected css class to the parent <li>. The css class causes a checkmark icon to appear over the <li>.

In Firefox, the selected css class is only applied after a user has gone through and checked each radio button at least once. I debugged and found that this is because of the way the self.Color computed observable is evaluated in the BackColorOption closure. Before the first time a radio is checked, typeof(self.Color) == 'function' evaluates to true. However after it is checked, typeof(self.Color) == 'string' evaluates to true.

This typeof(self.Color) behavior is the same according to both Firebug and Chrome's js debugger. However the issue in FF is because of this line in the self.Selected computed observable in the BackColorOption closure:

return backColor.toLowerCase() == self.Color;

Chrome & IE still return true even when self.Color is a function instead of a string. However Firefox does not. When self.Color is a function, it returns false. This is why you have to check each radio at least once before the css class is added to the <li> and the icon appears.

I'm still a bit new to knockout, and may not be appropriately calling a viewmodel property as a function when supposed to. I'm still a little unclear when to use the () parenthesis and when to omit them. Is there another way I should write the self.Selected computed observable, which depends on the self.Color computed observable (in the BackColorOption closure)?

Update 1

I was able to get this to work in FF 10.0.2 with the following:

self.Selected = ko.computed(function () {
    var backColor = self.parent.BackColor();
    var selfColor = self.Color;
    if (typeof (selfColor) === 'function')
        selfColor = self.Color();
    if (backColor) {
        return backColor.toLowerCase() === selfColor;
    }
    return false;
});

However, this feels like I'm fighting knockout. Isn't it supposed to "just work"?

Was it helpful?

Solution

The value binding in KO is not really ideal for radio buttons and checkboxes. In your situation you need your radio buttons to have a value attribute so that when you click them, that value can be used to update your TestViewModel.BackColor observable.

Normally with radio buttons, you don't want the value attribute to change (if ever) once the html is rendered.

So, I've changed your html template from using a value binding to instead using an attr binding (html attribute). This is now just setting the value html attribute of your radio buttons. The checked binding then keeps your TestViewModel.BackColor observable synced with whatever the value is of the radio button that is checked.

See this fiddle: http://jsfiddle.net/m2KQ2/

Also, the line in your BackColorOption function has a typo:

self.Selected = ko.computed(function () {
    var backColor = self.parent.BackColor();
    if (backColor) {
        return backColor.toLowerCase() == self.Color; //<-- should be Color();
    }
    return false;
});
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top