Peter G.
Peter G.

Reputation: 8034

Knockout binding for Bootstrap toggle buttons

I'm looking for a way in KnockoutJS to bind toggle button to a function that gets called when a specific value (button) is selected.

I tried several solutions and got it running with just KnockoutJS, but when I add Boostrap to it, it still doesn't work.

My code shown here does not trigger the handler function associated with checked event. For the latest working example, use the JSFiddle.

HTML:

<div id="access-rights-control" class="btn-group pull-right" data-toggle="buttons">
  <label class="btn btn-xs">
    <input type="radio" name="status" value="1" id="status-published" data-bind="checked: $root.allowAccessHandler" />Allow access
  </label>
  <label class="btn btn-xs active">
    <input type="radio" name="status" value="0" id="status-draft" checked /> No change
  </label>
</div>

JS:

var viewModel = function() {
  this.allowAccessHandler = function() {
    alert("Works!");
  }
};
ko.applyBindings(new viewModel());

JSFiddle: http://jsfiddle.net/p8nSe/191/

Upvotes: 1

Views: 7297

Answers (4)

Peter G.
Peter G.

Reputation: 8034

Finally I found an approach that works natively and gets the task done. I'm not sure if it is the most generic one, since I have to override ko.bindingHandlers.radio, but it works.

ko.bindingHandlers.radio = {
      init: function(element, valueAccessor, allBindings, data, context) {
        var $buttons, $element, observable;
        observable = valueAccessor();
        if (!ko.isWriteableObservable(observable)) {
          throw "You must pass an observable or writeable computed";
        }
        $element = $(element);
        if ($element.hasClass("btn")) {
          $buttons = $element;
        } else {
          $buttons = $(".btn", $element);
        }
        elementBindings = allBindings();
        $buttons.each(function() {
          var $btn, btn, radioValue;
          btn = this;
          $btn = $(btn);
          radioValue = elementBindings.radioValue || $btn.attr("data-value") || $btn.attr("value") || $btn.text();
          $btn.on("click", function() {
            observable(ko.utils.unwrapObservable(radioValue));
          });
          return ko.computed({
            disposeWhenNodeIsRemoved: btn,
            read: function() {
              $btn.toggleClass("active", observable() === ko.utils.unwrapObservable(radioValue));
            }
          });
        });
      }
    };

    var viewModel = function() {
      this.radioSelected = ko.observable('0');
      this.radioSelected.subscribe(function(newValue) {
        console.debug("Comparing", newValue, '1');
        if (newValue === '1') {
          alert("Works!");
        }
      });
    };
    ko.applyBindings(new viewModel());
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<div class="btn-group">
  <button type="button" class="btn" data-bind="radio: radioSelected, radioValue: '1'">Allow access</button>
  <button type="button" class="btn" data-bind="radio: radioSelected, radioValue: '0'">No change</button>
</div>

JSFiddle: http://jsfiddle.net/p8nSe/192/

Upvotes: 0

Joel R Michaliszen
Joel R Michaliszen

Reputation: 4222

Maybe what you want is something like what the snippet below do. please see and gimme a feedback.

ko.bindingHandlers["bsChecked"]  ={
  init: function(element, valueAccessor){
    ko.utils.registerEventHandler(element,"change",function(event){
        var check = $(event.target);
        valueAccessor()(check.val());      
    });
  }
 
};

var viewModel = function() {
  
  this.checkedButton = ko.observable("2").extend({ notify: 'always' });
  
  this.checkedButton.subscribe( function(checkedVal) {
    // here you can decide what to do depends on the new checkedVal
    alert("Works! : "+ checkedVal);
  },this); 
  
};
ko.applyBindings(new viewModel());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<div id="access-rights-control" class="btn-group pull-left" data-toggle="buttons">
    <label class="btn btn-xs btn-default">
    <input type="radio" name="status" value="1" data-bind="bsChecked: checkedButton" />Allow access
  </label>
  <label class="btn btn-xs btn-default">
    <input type="radio" name="status" value="2" data-bind="bsChecked: checkedButton" />No change
  </label>
</div>

Update

Created a binding handler to take care about the prevent changes caused by bootstrap, so this is a workaround and I suggest to you read about KO custom bindings and implement the update function of the binding handler.

Upvotes: 4

Roy J
Roy J

Reputation: 43881

I think you have two issues:

  1. You don't have a binding handler for the Bootstrap radio group, which means that your checked binding isn't going to work
  2. You aren't using the checked binding correctly.

I can help you with the 2nd. In a radio group where all the elements have the same name attribute and different value attributes, the variable bound to the checked observable will get the value of the selected radio item.

You can subscribe to the observable and take action when it gets a particular value.

var viewModel = function() {
  this.radioSelected = ko.observable();
  this.radioSelected.subscribe(function(newValue) {
    console.debug("Comparing", newValue, '1');
    if (newValue === '1') {
      alert("Works!");
    }
  });
  this.radioSelected('0');
  this.output = ko.observableArray();
};
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="access-rights-control" class="btn-group pull-right" data-toggle="buttons">
  <label class="btn btn-xs btn-default">
    <input type="radio" name="status" value="1" data-bind="checked: radioSelected" />Allow access
  </label>
  <label class="btn btn-xs btn-default active">
    <input type="radio" name="status" value="0" data-bind="checked: radioSelected" />No change
  </label>
</div>
<div data-bind="foreach:output">
  <div data-bind="text:$data">
  </div>
</div>

Again, this won't work if you have Bootstrap-styled radio buttons, because Bootstrap fiddles with the DOM in ways that Knockout isn't set up to detect. So you need a binding handler that works with Bootstrap radio magic. That may require that your approach be slightly different (ie, you may not use the checked binding) I don't know.

Upvotes: 1

Matt Burland
Matt Burland

Reputation: 45135

If you want something to run when you click on the input, then you need to use an event binding not the checked binding (which just binds the checked state to an observable, it's not an event handler). Something like:

var viewModel = function() {
  this.allowAccessHandler = function() {
    alert("Works!");
  }
};
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="access-rights-control" class="btn-group pull-right" data-toggle="buttons">
  <label class="btn btn-xs">
    <input type="radio" name="status" value="1" id="status-published" data-bind="event: { change: $root.allowAccessHandler }" />Allow access
  </label>
  <label class="btn btn-xs active">
    <input type="radio" name="status" value="0" id="status-draft" /> No change
  </label>
</div>

Now if you have the checked state bound to a property in your view model, it would probably be better to just subscribe to that property instead of attaching an event handler to the DOM as that would be more inline with separating your view and view model.

Upvotes: 1

Related Questions