Home » Html » Knockout + Bootstrap 3 Radio Buttons

Knockout + Bootstrap 3 Radio Buttons

Posted by: admin November 29, 2017 Leave a comment

Questions:

Related to: Bootstrap Radio Button Group

HTML:

<div class="btn-group" data-toggle="buttons">
    <label class="btn btn-primary">
        <input type="radio" name="options" id="option1" value="1" data-bind="checked: optionsValue"> Option 1
    </label>
    <label class="btn btn-primary">
        <input type="radio" name="options" id="option2" value="2" data-bind="checked: optionsValue"> Option 2
    </label>
    <label class="btn btn-primary">
        <input type="radio" name="options" id="option3" value="3" data-bind="checked: optionsValue"> Option 3
    </label>
</div>
<br />
<span data-bind="text: optionsValue"></span>

Javascript:

var ViewModel = function() {
    this.optionsValue = ko.observable()
};

ko.applyBindings(new ViewModel());

JsFiddle:


I have the above code which I’m trying to get working as I expect it to. The problem is that when data-toggle="buttons" is added to the btn-group div (as in the Bootstrap 3 example) the knockout binding stops working. If I leave the data-toggle off of the buttons group then the binding works as expected but the button group looks awful. I know that this didn’t work in Bootstrap 2 because they didn’t actually use the radio input for their radio styling. How come it refuses to work now even though they do?

Answers:

The bootstrap buttons and the knockout checked binding are still not playing nice:

  • knockout uses the click event inside the checked binding to tigger the underlaying observable to change
  • bootstrap subscribes on the click event to do the toggling but calls e.preventDefault() so KO won’t be notified about the click.

One possible solution is to create a custom binding handler where you subscribe on the change event (this is fired by bootstrap on toogle) and set your observables value there:

ko.bindingHandlers.bsChecked = {
    init: function (element, valueAccessor, allBindingsAccessor,
    viewModel, bindingContext) {
        var value = valueAccessor();
        var newValueAccessor = function () {
            return {
                change: function () {
                    value(element.value);
                }
            }
        };
        ko.bindingHandlers.event.init(element, newValueAccessor,
        allBindingsAccessor, viewModel, bindingContext);
    },
    update: function (element, valueAccessor, allBindingsAccessor,
    viewModel, bindingContext) {
        if ($(element).val() == ko.unwrap(valueAccessor())) {
             setTimeout(function () {
                $(element).closest('.btn').button('toggle');
             }, 1); 
        }
    }
}

And use it in your view with:

<label class="btn btn-primary">
    <input type="radio" name="options" id="option1" value="1" 
           data-bind="bsChecked: optionsValue"> Option 1
</label>

Original demo using Bootstrap 3.0.2 JSFiddle.
Updated demo using Bootstrap 3.2.0 JSFiddle.

Questions:
Answers:

I can’t take credit for this since once on my coworkers came up with it but it works really well.

<div class="btn-group" data-toggle="buttons">
    <label data-bind="css: { active: !HideInLeaderboards() }, 
                      click: function () { HideInLeaderboards(false); },
                      clickBubble: false" 
                      class="btn btn-default">
        Show Name
    </label>
    <label data-bind="css: { active: HideInLeaderboards() },
                      click: function () { HideInLeaderboards(true); },
                      clickBubble: false" class="btn btn-default">
        Hide Name
    </label>
</div>

Questions:
Answers:

change the handler @nemesv proposed to be this: and it worked in my app just fine.

ko.bindingHandlers.bsChecked = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var value = valueAccessor();
        var newValueAccessor = function () {
            return {
                change: function () {
                    value(element.value);
                }
            }
        };
        if ($(element).val() == ko.unwrap(valueAccessor())) {
            $(element).closest('.btn').button('toggle');
        }
        ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, viewModel, bindingContext);
    }
}

Questions:
Answers:

Knockstrap appears to be a binding between Bootstrap and Knockout, and it appears to be kept pretty well up to date. Concerning radio buttons in specific, they utilize this code:

// Knockout checked binding doesn't work with Bootstrap radio-buttons
ko.bindingHandlers.radio = {
    init: function (element, valueAccessor) {

        if (!ko.isObservable(valueAccessor())) {
            throw new Error('radio binding should be used only with observable values');
        }

        $(element).on('change', 'input:radio', function (e) {
            // we need to handle change event after bootsrap will handle its event
            // to prevent incorrect changing of radio button styles
            setTimeout(function() {
                var radio = $(e.target),
                    value = valueAccessor(),
                    newValue = radio.val();

                value(newValue);
            }, 0);
        });
    },

    update: function (element, valueAccessor) {
        var $radioButton = $(element).find('input[value="' + ko.unwrap(valueAccessor()) + '"]'),
            $radioButtonWrapper;

        if ($radioButton.length) {
            $radioButtonWrapper = $radioButton.parent();

            $radioButtonWrapper.siblings().removeClass('active');
            $radioButtonWrapper.addClass('active');

            $radioButton.prop('checked', true);
        } else {
            $radioButtonWrapper = $(element).find('.active');
            $radioButtonWrapper.removeClass('active');
            $radioButtonWrapper.find('input').prop('checked', false);
        }
    }
};