Problem
Radio buttons are hard-to-see, not easy to select, and let's face it, quite mundane. You would like to replace these radio buttons with a group of buttons that represent the same functionality, e.g., only one of the options may be selected at any given time.
Solution
Leveraging Bootstrap which provides many incredibly styled components for buttons, alert boxes, tables, forms, etc., regular radio buttons will be replaced by a button group (see screenshot below). Knockout.js will be used to create a custom data binding that will make the group of buttons act like regular radio buttons (with a nicer look of course).
This example assumes you have a basic understanding of both Bootstrap and Knockout.js.
The versions used for this example are 3.3.4 for Bootstrap and 3.3 for Knockout.js. This example should be compatible with older versions of these frameworks.
Discussion
Before getting started, the frameworks required must be setup. Bootstrap can be installed either via their CDN or downloaded. This example will use the CDN; however, I suggest you use the option that bests suits your needs. Knockout.js has been downloaded and saved into the same location as where the example lives. Ensure you update this location based on your project's location. And finally, a little bit of jQuery is used within the Knockout Custom Binding. Once again, this example is leveraging the CDN; however, you may download and include it if you like.
For this example to work, it requires two things. A group of buttons that will contain what would be the options if they were radio buttons and a Knockout observable value that will be used in the data-binding. This observable will contain the selected option. Below contains the HTML markup and the extremely basic Knockout ViewModel.
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
</head>
<body>
<div class="btn-group">
<button type="button" class="btn btn-default"
data-bind="radioButtonGroup: selectedOption, radioValue: 'option1'">Option 1</button>
<button type="button" class="btn btn-default"
data-bind="radioButtonGroup: selectedOption, radioValue: 'option2'">Option 2</button>
<button type="button" class="btn btn-default"
data-bind="radioButtonGroup: selectedOption, radioValue: 'option3'">Option 3</button>
</div>
<script src="knockout.js"></script>
<script src="http://code.jquery.com/jquery-2.1.3.min.js"></script>
<script>
function ViewModel() {
var self = this;
self.selectedOption = ko.observable();
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
</script>
</body>
</html>
This example won't work just yet. If you look closely, each of the HTML buttons contain a data-binding called radioButtonGroup
(this will be defined momentarily). This is the custom data binding that will accomplish our fancy button group. Supplied to the new data binding is the observable variable, aptly named, selectedOption
. A second value is supplied as well named radioValue
. This should be set to the value you wish the selectedOption
variable to contain when the user presses that button.
To complete this example, the custom binding needs to be created. Custom bindings are added under the ko.bindingHandlers
namespace. A binding handler is defined by creating an init
and update
function. Either are optional, but at least one must be defined. The init
function is called when Knockout is first data bound to the page. This is where you would create event listeners, etc. The update
function is called every time the value changes (including on first load). This function would be used if you wished to perform specific actions each time the value changed.
The radioButtonGroup
custom binding only requires the init
function because event listeners for the click event will be used to track all changes. The following JavaScript code should be placed (or included from a separate file) after the Knockout framework is included, but before your ViewModel is defined and the Knockout bindings are applied.
<script>
ko.bindingHandlers.radioButtonGroup = {
init: function (element, valueAccessor, allBindings, viewModel, 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));
$btn.toggleClass("btn-info", observable() === ko.utils.unwrapObservable(radioValue));
}
});
});
}
};
</script>
The init
function accepts 5 parameters. The element being data bound, the variable it is data bound too, all other bindings on this element, the entire ViewModel
(however this is being deprecated), and the bindingContext
, this is the new way proposed to access the ViewModel
by using bindingContext.$data
.
Inside the init
function, it first verifies it is dealing with an observable property. Using the HTML element with the data binding (and some jQuery), the related buttons are found and stored in a variable. These buttons are then looped through and the click event is listened for. When it occurs, the observable property associated to the data binding's value is updated with the option selected. And finally a computed variable is defined, that if the observable's value is equal the value of the button, the class active
and btn-info
are added to identify which button is currently selected.
That completes this example, as always you can find the full source code on GitHub.