Jason Parker
Jason Parker

Reputation: 4994

Why is KnockoutJs unaware of change to radioButton made by JQuery?

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

Answers (5)

Mr. Young
Mr. Young

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

Jason Parker
Jason Parker

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:

  • 'fancyDivRadio' must be bound to a div that contains a radio button. (No, I didn't want to automatically generate the radio button inside the div, which would constrain either my future options for styling or the ability to include other items in the div as well.)
  • As you can see by clicking around the jsFiddle link below, the viewModel is updated regardless of where in the colored DIV you click. Works like a charm! :)
  • The downside is that I ended up writing so much more code!!! (As opposed to the more direct jQuery method suggested by BenjaminPaul, which was only 3 lines of code.)
  • The upside is I can reuse this code on other pages with radio buttons, where I want the radio buttons to be easier for the user to click.
  • I could even add some jQuery animations to the DIV using this custom binding approach, which is pretty cool... it really gives you a fine degree of control!
  • I will most likely cut and paste this custom binding into a separate JS file where I can start to accumulate future custom bindings.

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

Anders
Anders

Reputation: 17564

The checked binding triggers on click not value chamge event I made a label binding for just this scenario

http://jsfiddle.net/CkEMa/7/

<input data-bind="checked: HourlyOrSalary, label: { caption: 'Hourly'}"
/></div>

Upvotes: 1

Josh E
Josh E

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());
    });

http://jsfiddle.net/CkEMa/8/

Upvotes: 0

BenjaminPaul
BenjaminPaul

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

Related Questions