5

I'm trying to replace a checkbox in a table with this good looking bootstrap switch.

I'm also using knockout and the binding handler as suggested by this link.

However I cannot get the binding handler to work and or the switch to be clickable in the table built using a knockout for each.

I've replicated the issue in a jsfiddle.

I'm still learning javascript and knockout and just cannot figure out what's going wrong in the binding handler or switch.

Any ideas?

The Html:

<div>
<input type="checkbox" data-bind="checked: on" />
</div>
<div>
    <span data-bind="text: on() ? 'on' : 'off'"></span>
</div>
<div class="switch switch-large" data-bind="bootstrapSwitchOn: on">
    <input type="checkbox" />
</div>

<p>Why doesn't this work in a table?</p>

<table class="table table-striped table-condensed table-bordered">
    <thead>
    <tr>
        <th>Name</th>
        <th>Old</th>
        <th>Old toggle With CheckBox</th>
        <th>Old toggle With Switch</th>
    </tr>
    </thead>
    <tbody data-bind="foreach: items">
        <tr>
            <td data-bind="text: Name"></td>
            <td data-bind="text: Old"></td>
            <td>
                <input type="checkbox" data-bind="checked: Old" />
            </td>
            <td>
                <div class="switch switch-large" data-bind="bootstrapSwitchOn: Old">
                    <input type="checkbox" />
                </div>
            </td>
        </tr>
    </tbody>
</table>

The binding handler and knockout view

/**
 * Knockout binding handler for bootstrapSwitch indicating the status
 * of the switch (on/off): https://github.com/nostalgiaz/bootstrap-switch
 */
ko.bindingHandlers.bootstrapSwitchOn = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        $elem = $(element);
        $(element).bootstrapSwitch('setState', ko.utils.unwrapObservable(valueAccessor())); // Set intial state
        $elem.on('switch-change', function (e, data) {
            valueAccessor()(data.value);
        }); // Update the model when changed.
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var vStatus = $(element).bootstrapSwitch('status');
        var vmStatus = ko.utils.unwrapObservable(valueAccessor());
        if (vStatus != vmStatus) {
            $(element).bootstrapSwitch('setState', vmStatus);
        }
    }
};

var vm = {
    on: ko.observable(true),

    items: ko.observableArray([
        {Name: "Dave", Old: ko.observable(false)},
        {Name: "Richard", Old: ko.observable(true)},
        {Name: "John", Old: ko.observable(false)}
    ])    
}

ko.applyBindings(vm);
Dave
  • 317
  • 4
  • 11

2 Answers2

2

You were using an older version of bootstrapSwitch (1.3) which automatically executes the plugin on elements with '.switch' class, giving the impression things were working with the custom binding.

I've updated the plugin to the latest version, and am initiating the plugin within the init binding method, eg:

ko.bindingHandlers.bootstrapSwitchOn = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        $elem = $(element);
        $(element).bootstrapSwitch(); // initiate the plugin before setting options
        $(element).bootstrapSwitch('setState', ko.utils.unwrapObservable(valueAccessor())); // Set intial state
        // .. etc
    }
}

Upgrading the latest version of the plugin appears to fix things: https://jsfiddle.net/badsyntax/59wnW/5/

[UPDATE] - I think the root cause of the issue is a race conditional between the plugin initiating itself on DOM ready, and between knockout building the DOM.

This example works with the old version of bootstrapSwitch, as long as you initiate the plugin after binding the viewmodel: https://jsfiddle.net/badsyntax/59wnW/6/

Syscall
  • 19,327
  • 10
  • 37
  • 52
badsyntax
  • 9,394
  • 3
  • 49
  • 67
  • That got it. I'm using the latest version in my code (must remember to check version of resources when forking a fiddle) but I'm also using the class make-switch so successfully breaking it. I've modified my init on the binding handler and it works like a charm. – Dave Sep 17 '13 at 08:42
  • Setting it up like this do I need to worry about memory leaks when knockout tears down the list when the observable array changes? – Dave Sep 17 '13 at 08:46
  • I guess you'd want to destroy the plugin when the associated element gets destroyed by knockout. Refer to this link: http://www.knockmeout.net/2011/07/another-look-at-custom-bindings-for.html and look for point 5, or search for `ko.utils.domNodeDisposal.addDisposeCallback` – badsyntax Sep 17 '13 at 08:51
  • Thank you very much. Great links. Moving from C# winforms and finding that there's so much to learn and so may frameworks to know :-) – Dave Sep 17 '13 at 10:18
2

Here's the Knockout binding for v3.2.1 of BootstrapSwitch (http://www.bootstrap-switch.org/):

ko.bindingHandlers.bootstrapSwitchOn = {
  init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
    var $elem;
    $elem = $(element);
    $(element).bootstrapSwitch();
    $(element).bootstrapSwitch("state", ko.utils.unwrapObservable(valueAccessor()));
    $elem.on("switchChange.bootstrapSwitch", function(e, data) {
      valueAccessor()(data);
    });
  },
  update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
    var vStatus, vmStatus;
    vStatus = $(element).bootstrapSwitch("state");
    vmStatus = ko.utils.unwrapObservable(valueAccessor());
    if (vStatus !== vmStatus) {
      $(element).bootstrapSwitch("state", vmStatus);
    }
  }
};
ProfNimrod
  • 4,142
  • 2
  • 35
  • 54