Justin Helgerson
Justin Helgerson

Reputation: 25551

Knockout checked binding not working

I am using the Twitter Bootstrap button group along with Knockout. I feel like I am overlooking something very simple, but, I haven't been able to get the checked binding working in this instance.

I have a jsFiddle reproducing the problem here: http://jsfiddle.net/n5SBa/.

Here is the code from the fiddle:

HTML

<div class="form-group">
    <div class="btn-group" data-toggle="buttons">
        <label class="btn btn-primary" data-bind="click: ClickScore.bind($data, '0'), css: { active: Score() == '0' }">
            <input type="radio" name="score" value="0" data-bind="checked: Score" /> 0
        </label>
        <label class="btn btn-primary" data-bind="click: ClickScore.bind($data, '1'), css: { active: Score() == '1' }">
            <input type="radio" name="score" value="1" data-bind="checked: Score" /> 1
        </label>
        <label class="btn btn-primary" data-bind="click: ClickScore.bind($data, '2'), css: { active: Score() == '2' }">
            <input type="radio" name="score" value="2" data-bind="checked: Score" /> 2
        </label>
        <label class="btn btn-primary" data-bind="click: ClickScore.bind($data, '3'), css: { active: Score() == '3' }">
            <input type="radio" name="score" value="3" data-bind="checked: Score" /> 3
        </label>
        <label class="btn btn-primary" data-bind="click: ClickScore.bind($data, '4'), css: { active: Score() == '4' }">
            <input type="radio" name="score" value="4" data-bind="checked: Score" /> 4
        </label>
        <label class="btn btn-primary" data-bind="click: ClickScore.bind($data, '5'), css: { active: Score() == '5' }">
            <input type="radio" name="score" value="5" data-bind="checked: Score" /> 5
        </label>
    </div>
</div>

<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>

Javascript

function SurveyViewModel() {
    var self = this;

    self.Score = ko.observable();

    //Events
    self.ClickScore = function (score) {
        self.Score(score);
    };

    //Computations
    self.RecommendationLabel = ko.computed(function () {
        if (self.Score() < 8) {
            return "Some question?";
        } else {
            return "Some other question?";
        }
    });

    self.DOMSelectedScore = ko.computed(function() {
        if ($('input[name=score]:checked').val()) {
            return $('input[name=score]:checked').val();
        } else {
            return 'no value!';   
        }
    });
};

var surveyViewModel = new SurveyViewModel();

ko.applyBindings(surveyViewModel);

In the example, I'm unable to get the actual radio button selected in the DOM so that it can be properly submitted in my form.

Upvotes: 2

Views: 9067

Answers (2)

Markus Jarderot
Markus Jarderot

Reputation: 89241

The DOMSelectedScore computed observable doesn't reference any other observable, so it never gets recalculated.

self.DOMSelectedScore = ko.computed(function() {
    self.Score(); // subscribe to Score, even if we don't use it.
    var val = $('input[name=score]:checked').val();
    return val || 'no value!';
});

Fixing that, it appears that the DOM isn't updated until after the function returns, so the value lags behind one click. To fix this, we need to delay until the DOM has been updated:

self.DOMSelectedScore = ko.computed(function() {
    self.Score(); // subscribe to Score, even if we don't use it.
    var val = $('input[name=score]:checked').val();
    return val || 'no value!';
}).extend({throttle:1}); // Wait for the DOM update to complete

http://jsfiddle.net/n5SBa/5/


To simplify the binding of numbers with knockout, you could define an extension method for observables:

ko.observable.fn.asString = function () {
    var source = this;
    if (!source._asString) {
        source._asString = ko.computed({
            read: function () {
                return String(source());
            },
            write: function (newValue) {
                source(Number(newValue));
            }
        });
    }
    return source._asString;
};

And then

<input type="radio" name="score" value="2" data-bind="checked: Score.asString()"/>

Upvotes: 4

Michael Best
Michael Best

Reputation: 16688

The values of the checkboxes are strings ("0", "1", etc.), but you are setting the value of the observable to a number. The checked binding uses strict equality when doing the comparison and doesn't consider the number 1 to equal the string "1".

You can fix this by setting the value of the observable to strings:

data-bind="click: ClickScore.bind($data, '1')"

http://jsfiddle.net/mbest/n5SBa/2/

Upvotes: 9

Related Questions