Reputation: 4994
Update: I posted my code solution as an answer down below, which could help if someone wants to see a complete (and fairly simple) example of a KnockoutJs custom binding.
Problem:
When I use jQuery to set the checked status of a radio button... then it seems as if my KnockoutJs viewmodel does not track this change!
Scenario:
I have multiple large DIVs, and each DIV wraps one radio button. (This makes it easier for users to click the radio button by having a larger area to click on.) When the user clicks somewhere in the div, I want to check the radio button for them.... which works just fine. However, when attempting to read the value from the viewModel property bound to this radio button.... it has not been updated. :-(
The only time the viewModel is updated is if I click DIRECTLY on the radio button inside the div. If I just click somewhere inside the div (which executes my jQuery), then.... even though the radio visibly becomes checked.... the knockoutjs viewModel property has not been updated with a new value.
Question: Can someone please tell me how to change the checked status of a radio button using jQuery and have KnockoutJs play nicely and be updated as well?
Code is below, and here is jsFiddle: http://jsfiddle.net/CkEMa/67/
<script>
$(document).ready(function ()
{
var self = this;
function ViewModel()
{
this.HourlyOrSalary = ko.observable("");
}
viewModel = new ViewModel();
ko.applyBindings(viewModel, document.getElementById('divKnockout'));
// Click event for DIV around radio button
$('.divRadioWrapper').click(function ()
{
var radio = $(this).find('input[type="radio"]');
radio.prop('checked', true);
});
// Just for testing...
$('#testButton').click(function ()
{
var viewModelVal = viewModel.HourlyOrSalary();
alert('Value --> ' + viewModelVal);
});
});
</script>
<style>
.divRadioWrapper {
background-color: #dde9f5; /*#d8f5f0;*/ /* #dcfbff; */
width: 75px;
padding: 5px 10px;
border: 1px solid lightgray;
cursor: pointer;
margin-bottom: 10px;
}
</style>
<div id="divKnockout">
<div class="divRadioWrapper">
<input type="radio" name="formType" value="hourly" data-bind="checked: HourlyOrSalary"
/>Hourly</div>
<div class="divRadioWrapper">
<input type="radio" name="formType" value="salary" data-bind="checked: HourlyOrSalary"
/>Salary</div>
<br />
<input type="button" id="testButton" value="Display viewModel data" />
</div>
Upvotes: 0
Views: 7127
Reputation: 2424
This is probably best handled by using the built-in KnockOut Checked Binding Handler. Alternatively you could build a much more flexible handler using a custom binding handler. http://knockoutjs.com/documentation/custom-bindings.html
There is an excellent tutorial available too. http://learn.knockoutjs.com/#/?tutorial=custombindings
The point of Knockout is to provide data-binding handlers and logic control so you don't have to waste time doing it otherwise.
You can see the binding in action via a pre tag.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.8.3.js"></script>
<script type="text/javascript" src="http://knockoutjs.com/downloads/knockout-2.2.1.debug.js"></script>
</head>
<body>
<input type="radio" name="formType" value="hourly" data-bind="checked: HourlyOrSalary" />Hourly
<input type="radio" name="formType" value="salary" data-bind="checked: HourlyOrSalary" />Salary
<br />
<input type="button" id="testButton" value="Display viewModel data" />
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
<script>
$(document).ready(function()
{
var viewModel = {
HourlyOrSalary: ko.observable("")
};
ko.applyBindings(viewModel);
});
</script>
</body>
</html>
I just wanted to add to this here... the point of Knockout is to follow DRY principals (Don't Repeat Yourself), and to practice good separation of the Model (pure JSON), ViewModel (observable Model + computed logic + validation), and View (HTML). This separation is key when it comes cutting out the stuff that you usually have to write for sending and receiving your data to the server, displaying that data to the user, and validating any user input before the whole operation starts over.
You could just hard/hand code the whole thing. It's your own time (or foot) so patterns like these are intended to save you time (now and probably more late) when you are writing a large solution without having to duplicate functionality (read I didn't say code here because functionality can be the same with different code paths).
Upvotes: 2
Reputation: 4994
Thanks
Thanks everyone for your answers. In particular, Mr. Young helped me realize I was probably thinking about it in the wrong way from the beginning. I went ahead and implemented my first Custom Binding for knockoutjs, following the tutorials he listed. While the answer BenjaminPaul posted would certainly do the trick via jQuery, I believe the Custom Binding approach is indeed a more clean solution that allows for the separations of concerns that Mr. Young spoke about.
Solution
I wanted to share the solution I ended up creating below, which is a knockoutjs custom binding called fancyDivRadio. :-)
Things to note:
Try it out here!: http://jsfiddle.net/CkEMa/66/ (Note: Fixed jsFiddle link. Github KO url was broken.)
The code:
<script>
$(document).ready(function ()
{
ko.bindingHandlers.fancyDivRadio = {
init: function (element, valueAccessor)
{
// Get radio button located inside this div
var radio = $(element).find('input[type="radio"]');
// When div is clicked, check the radio and trigger radio change event
$(element).click(function ()
{
radio.prop('checked', true);
radio.change();
});
// When radio button is checked, update the viewModel property!!
$(radio).change(function ()
{
if (radio.prop('checked')) // only if it was changed to checked
{
// Set viewModel property to value of the radio button that was clicked
var value = valueAccessor();
value(radio.val());
}
});
},
update: function (element, valueAccessor, allBindingsAccessor)
{
var value = valueAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(value);
// Get radio button located inside this div
var radio = $(element).find('input[type="radio"]');
// Set radio to be checked or unchecked
var shouldBeChecked = valueUnwrapped == radio.val();
radio.prop('checked', shouldBeChecked);
}
};
function ViewModel()
{
this.HourlyOrSalary = ko.observable("");
}
viewModel = new ViewModel();
ko.applyBindings(viewModel);
});
</script>
<style>
.divRadioWrapper {
background-color: #dde9f5;
width: 75px;
padding: 5px 10px;
border: 1px solid lightgray;
cursor: pointer;
margin-bottom: 10px;
}
</style>
<div>
<div class="divRadioWrapper" data-bind="fancyDivRadio: HourlyOrSalary">
<input type="radio" name="formType" value="hourly" />
Hourly
</div>
<div class="divRadioWrapper" data-bind="fancyDivRadio: HourlyOrSalary">
<input type="radio" name="formType" value="salary" />
Salary
</div>
<br />
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
</div>
Upvotes: 4
Reputation: 17564
The checked binding triggers on click not value chamge event I made a label binding for just this scenario
<input data-bind="checked: HourlyOrSalary, label: { caption: 'Hourly'}"
/></div>
Upvotes: 1
Reputation: 7432
No custom binders needed, just fix a spelling mistake (divKnockous instead of divKnockout) and use your viewmodel to change the value:
// Click event for DIV around radio button
$('div.divRadioWrapper').on("click", function () {
viewModel.HourlyOrSalary($(this).find(":radio").val());
});
Upvotes: 0
Reputation: 2931
I would set the value on the viewmodel and allow the change notification to check the box for you...
// Click event for DIV around radio button
$('.divRadioWrapper').click(function ()
{
var newValue = !viewModel.HourlyOrSalary();
viewModel.HourlyOrSalary(newValue);
});
Upvotes: 2