Knockout + Bootstrap 3 Radio Buttons
Asked Answered
S

6

32

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?

Silvey answered 19/11, 2013 at 16:59 Comment(2)
The Bootstrap toogle buttons still not playing nice with KO so you need a custom binding handler something like this: jsfiddle.net/hd89yScurry
Note that Radio Button groups using this code did work properly in Bootstrap 2.x (though they also showed the Radio Button itself inside the "button" unless you hid it yourself). This is because the Bootstrap 2.x code did not try to intercept the clicks - it preferred to use actual button elements to simulate this same behavior.Octillion
S
50

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.

Scurry answered 19/11, 2013 at 20:0 Comment(12)
@Kittoes you should ask the author about it...Scurry
lol, sorry man. Just wondering if there as some obvious reason behind it as I'm pretty new to this javascript thing. Your answer worked quite well it's just a shame that something like that is necessary.Silvey
I'm curious how one would get this to work on a single checkbox in addition to the radio buttons.Silvey
Excellent post! Here's an adaptation for the checkbox button group: jsfiddle.net/MEA33/1Dives
Thank you. I am using this code and it is working except in a true IE 8 browser (crashes browser). I created a question here: #21536947 any help would be appreciatedPyrethrum
@Pyrethrum I've seen your question already and I was able to repro the issue in IE8 but I have no idea why it crahes. So you will need some advanced bedugging techniques to find out exactly what causes the IE crash.Scurry
@Scurry This is broken again with bootstrap 3.1.1, clicking won't show the toggled button, but changing the value from vm works - jsfiddle.net/a4bc2/2Eustache
@Eustache interesting. I don't not sure what has been changed but as a quick fix a setTimeout in the update handler fixes the problem: jsfiddle.net/9PF9qScurry
@Eustache similar problem here. Data is ok, but BS & KO both toggle the value. Adding && !$(element).prop('checked')) condition to update fixed it for meBrietta
@Scurry This appears to be broken again in 3.2.0, can someone advise how to fix it? jsfiddle.net/a4bc2/5Endemic
@CCInc with some modificaiton (an additional setTimeout) it is working again: jsfiddle.net/22osabd0Scurry
any ideas for 4.0 bootstrap?Rhea
B
15

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>
Beasley answered 13/3, 2014 at 15:5 Comment(1)
Thanks, this works great for multiple linked toggles using the same observable!Octopod
H
4

There is a much easier approach found here.

We can just not use the data-toggle attribute and try to achieve the same behavior using knockout's data binding.

It's almost as simple as the native html radios.

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

ko.applyBindings(new ViewModel());
input[type="radio"] {
  /* Make native radio act the same as bootstrap's radio. */
  display: none;
}
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="btn-group">
  <label class="btn btn-primary" data-bind="css: {active: optionsValue() == '1'}">
    <input type="radio" name="options" id="option1" value="1" data-bind="checked: optionsValue">Option 1
  </label>
  <label class="btn btn-primary" data-bind="css: {active: optionsValue() == '2'}">
    <input type="radio" name="options" id="option2" value="2" data-bind="checked: optionsValue">Option 2
  </label>
  <label class="btn btn-primary" data-bind="css: {active: optionsValue() == '3'}">
    <input type="radio" name="options" id="option3" value="3" data-bind="checked: optionsValue">Option 3
  </label>
</div>
<br/>
<span data-bind="text: optionsValue"></span>
Hypoxanthine answered 17/7, 2018 at 17:22 Comment(0)
E
3

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);
        }
    }
};
Endemic answered 27/7, 2015 at 22:16 Comment(0)
H
1

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);
    }
}
Hyposensitize answered 13/10, 2014 at 21:25 Comment(0)
T
1

There's a simple solution here that I'm surprised hasn't been mentioned yet.

1) Remove the data-toggle="buttons" from the btn-group

Now knockout should be working

2) Bootstrap applies custom css for these inputs to hide them, that we just lost by removing the data-toggle, so apply the following css to the radio inputs:

position: absolute;
clip: rect(0,0,0,0);
pointer-events: none;

3) Last thing we need is for the selected option's button to have the active class. Add the following to the label buttons:

data-bind="css: {active: optionsValue() == valueOfThisInput}"
Tryma answered 30/11, 2018 at 17:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.