Binding true / false to radio buttons in Knockout JS
Asked Answered
J

8

86

In my view model I have a IsMale value that has the value true or false.

In my UI I wish to bind it to the following radio buttons:

<label>Male
   <input type="radio" name="IsMale" value="true" data-bind="checked:IsMale"/>
</label> 
<label>Female
   <input type="radio" name="IsMale" value="false" data-bind="checked:IsMale"/>
</label>

The problem I think is checked expects a string "true" / "false". So my question is, how can I get this 2-way binding w/ this UI and model?

Jandy answered 12/4, 2012 at 15:39 Comment(1)
For Knockout versions >= 3.0 see Natan's answer for a simpler soution than suggested by the accepted answer.Haland
C
81

One option is to use a writeable computed observable.

In this case, I think that a nice option is to make the writeable computed observable a "sub-observable" of your IsMale observable. Your view model would look like:

var ViewModel = function() {
   this.IsMale = ko.observable(true);

   this.IsMale.ForEditing = ko.computed({
        read: function() {
            return this.IsMale().toString();  
        },
        write: function(newValue) {
             this.IsMale(newValue === "true");
        },
        owner: this        
    });          
};

You would bind it in your UI like:

<label>Male
   <input type="radio" name="IsMale" value="true" data-bind="checked:IsMale.ForEditing"/>
</label> 
<label>Female
   <input type="radio" name="IsMale" value="false" data-bind="checked:IsMale.ForEditing"/>
</label>

Sample: http://jsfiddle.net/rniemeyer/Pjdse/

Crosseye answered 12/4, 2012 at 15:57 Comment(10)
Seems like a great solution. I use the mapping pluggin to refresh my VM. Do you know it that would wipe out ForEditing on IsMale?Jandy
If you are using the mapping plugin, then you would have to use the create callbacks (knockoutjs.com/documentation/…) in the mappings or add the ForEditing computed observable after your data has been initialized (and after new data has been added).Crosseye
Here is an alternative that pushes the computed observable creation into a custom binding, which would make it so you don't have to worry about dealing with it in the mapping plugin: jsfiddle.net/rniemeyer/utsvJCrosseye
Updated JS, because the managed resource on both of the above jsfiddles are 404'ing: jsfiddle.net/utsvJ/12Flue
+1 to using a binding instead of creating an observable on every objectParatroops
@RPNiemeyer's solution works awesome for me using the mapping pluginLesterlesya
@RPNiemeyer is definitely the way to go.Moonseed
Here is another version that uses extenders instead of bindings: jsfiddle.net/siimv/mQ7CU May come handy if you have different bindings where you have to use boolean's string value.Faradize
This solution definitely works, but it seems like such a hoop to have to jump through! Is binding to true/false with radio buttons that uncommon that it's not part of the knockout core functionality?Fadge
@RPNiemeyer thanks! that fiddle in the comments is the one that worked for me.Tobe
G
133

I know this is an old thread, but I was having the same problem and found out a much better solution that was probably added to knockout after this question was officially answered, so I'll just leave it for people with the same problem.

Currently there is no need for extenders, custom binding handlers or computeds. Just provide a "checkedValue" option, it will use that instead of the html 'value' attribute, and with that you can pass any javascript value.

<input type="radio" name="a" data-bind="checked:IsChecked, checkedValue: true"/>
<input type="radio" name="a" data-bind="checked:IsChecked, checkedValue: false"/>

Or:

<input type="radio" name="b" data-bind="checked:Quantity, checkedValue: 1"/>
<input type="radio" name="b" data-bind="checked:Quantity, checkedValue: 2"/>
<input type="radio" name="b" data-bind="checked:Quantity, checkedValue: 3"/>
Gurney answered 24/12, 2013 at 17:24 Comment(8)
Looking at the source, it seems to make a decision to use the checked value here. The 'useCheckedValue' is set to true if the input is a radio or a checkbox with the value as an array. Also, I'm using Knockout 3.0. See if that helps.Gurney
yeah, thanks, I've upgraded to 3.0.0 and now it's there. Still need to wrap my boolean values in a String because the server code was expecting those ;-)Herrod
Good info. Two additional points: (1) I found that with pre-v3.0 Knockout I was able to use the value binding and then the checked binding (in that order), but with 3.0 I had to use the checkedValue binding instead of the value binding. (2) pre-v3.0 was picky about requiring the value binding to precede the checked binding to function correctly, so I have a feeling that things might also work better in v3.0 in all scenarios if you put the checkedValue binding before the checked binding, like they show in the docs.Purpure
@ZiglioNZ one way this can fail is if your checked setter (or maybe something dependent on it) raises an error - then the correct checkbox isn't checkedCyler
I am using version 3.4 and finds that I still have to put in value="true" and the above to get it to work.Fillin
Somebody, please mark this as "the answer" - it's very clean.Unjust
One issue with this is that it leaves the radio button with the "false" unchecked. I really was looking to toggle between the two buttons and visually have it hold the checked. I needed add a method to reset the checked radio buttons.Baun
Ignore the prior comment. Must of had a caching issue. This works like a charm!Baun
C
81

One option is to use a writeable computed observable.

In this case, I think that a nice option is to make the writeable computed observable a "sub-observable" of your IsMale observable. Your view model would look like:

var ViewModel = function() {
   this.IsMale = ko.observable(true);

   this.IsMale.ForEditing = ko.computed({
        read: function() {
            return this.IsMale().toString();  
        },
        write: function(newValue) {
             this.IsMale(newValue === "true");
        },
        owner: this        
    });          
};

You would bind it in your UI like:

<label>Male
   <input type="radio" name="IsMale" value="true" data-bind="checked:IsMale.ForEditing"/>
</label> 
<label>Female
   <input type="radio" name="IsMale" value="false" data-bind="checked:IsMale.ForEditing"/>
</label>

Sample: http://jsfiddle.net/rniemeyer/Pjdse/

Crosseye answered 12/4, 2012 at 15:57 Comment(10)
Seems like a great solution. I use the mapping pluggin to refresh my VM. Do you know it that would wipe out ForEditing on IsMale?Jandy
If you are using the mapping plugin, then you would have to use the create callbacks (knockoutjs.com/documentation/…) in the mappings or add the ForEditing computed observable after your data has been initialized (and after new data has been added).Crosseye
Here is an alternative that pushes the computed observable creation into a custom binding, which would make it so you don't have to worry about dealing with it in the mapping plugin: jsfiddle.net/rniemeyer/utsvJCrosseye
Updated JS, because the managed resource on both of the above jsfiddles are 404'ing: jsfiddle.net/utsvJ/12Flue
+1 to using a binding instead of creating an observable on every objectParatroops
@RPNiemeyer's solution works awesome for me using the mapping pluginLesterlesya
@RPNiemeyer is definitely the way to go.Moonseed
Here is another version that uses extenders instead of bindings: jsfiddle.net/siimv/mQ7CU May come handy if you have different bindings where you have to use boolean's string value.Faradize
This solution definitely works, but it seems like such a hoop to have to jump through! Is binding to true/false with radio buttons that uncommon that it's not part of the knockout core functionality?Fadge
@RPNiemeyer thanks! that fiddle in the comments is the one that worked for me.Tobe
G
11

This works for me:

http://jsfiddle.net/zrBuL/291/

<label>Male
   <input type="radio" name="IsMale" value="1" data-bind="checked:IsMale"/>
</label> 
<label>Female
   <input type="radio" name="IsMale" value="0" data-bind="checked:IsMale"/>
</label>
Glutelin answered 12/4, 2012 at 15:54 Comment(6)
the only problem I see is that when serializing viewmodel to pass to server, you will get integers in observable (instead of booleans). You will need to call vm.IsMale(!!vm.+IsMale()); before serializing json to send to server (in case server side cannot handle it properly)Glutelin
I think you are right, but my Javascript knowledge is lacking. Can you please explain to me how !!+ works? I'm not familiar w/ that syntax.Jandy
@Jandy this converts string or number to boolean - chek this jsfiddle.net/nickolsky/6ydLZ ,if string is already bool it will keep it as boolGlutelin
Thanks very much. I think this is a great solution too.Jandy
It doesn't work both ways though. The radio buttons aren't checked when loaded.Venality
it works, this was just old fiddle - fixed it. At some point github does not allow linking of raw files from jsfiddle, so some old fiddles no longer working.Glutelin
D
1
ko.bindingHandlers['radiobuttonyesno'] = {
    'init': function (element, valueAccessor, allBindingsAccessor) {
        var stateHandler = function (property, allBindingsAccessor, key, value, checkIfDifferent) {
            if (!property || !ko.isObservable(property)) {
                var propWriters = allBindingsAccessor()['_ko_property_writers'];
                if (propWriters && propWriters[key])
                    propWriters[key](value);
            } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
                property(value);
            }
        };

        var updateHandler = function () {
            var valueToWrite;

            if ((element.type == "radio") && (element.checked)) {
                valueToWrite = element.value;
            } else {
                return; // "radiobuttonyesno" binding only responds to selected radio buttons
            }

            valueToWrite = (valueToWrite === "True") ? true : false;

            var modelValue = valueAccessor(), unwrappedValue = ko.utils.unwrapObservable(modelValue); //can be true of false

            stateHandler(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
        };
        ko.utils.registerEventHandler(element, "click", updateHandler);

        // IE 6 won't allow radio buttons to be selected unless they have a name
        if ((element.type == "radio") && !element.name)
            ko.bindingHandlers['uniqueName']['init'](element, function () { return true });
    },
    'update': function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());

        value = value ? "True" : "False";

        if (element.type == "radio") {
            element.checked = (element.value == value);
        }
    }
};

Use this binder instead of creating stupid ko computed observables.

Example:

<label>Male
        <input type="radio" name="IsMale" value="True" data-bind="radiobuttonyesno:IsMale"/>
     </label> 
     <label>Female
        <input type="radio" name="IsMale" value="False" data-bind="radiobuttonyesno:IsMale"/>
     </label>
Db answered 29/9, 2013 at 0:59 Comment(0)
B
1

Once you figure out that the initial match for the radio button wants to match only a string and wants to set the value to a string, it is simply a matter of converting your initial value to string. I had to fight this with Int values.

After you have setup your observables, convert the value to string and KO will do its magic from there. If you are mapping with individual lines, do the conversion in those lines.

In the example code, I'm using Json to map the whole Model in a single command. Then letting Razor insert the value between the quotes for the conversion.

script type="text/javascript">
    KoSetup.ViewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
    KoSetup.ViewModel.ManifestEntered("@Model.ManifestEntered");       //Bool
    KoSetup.ViewModel.OrderStatusID("@Model.OrderStatusID");           //Int
</script>

I use a "Dump it all to the screen" at the bottom of my web page during development.

<h4>Debug</h4>
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>

Here are the data values, Before

"OrderStatusID": 6,
"ManifestEntered": true,

and, After

"OrderStatusID": "6",
"ManifestEntered": "True",

In my project, I didn't need to convert Bools, because I'm able to use a checkbox that doesn't have the same frustration.

Bubaline answered 24/3, 2014 at 15:53 Comment(0)
S
0

Why not simply true and false instead of 1 and 0?

 <label>Male
    <input type="radio" name="IsMale" value="true" data-bind="checked:IsMale"/>
 </label> 
 <label>Female
    <input type="radio" name="IsMale" value="false" data-bind="checked:IsMale"/>
 </label>
Selfcontrol answered 28/4, 2013 at 4:47 Comment(1)
if you are sending json objects it makes difference.Selfcontrol
R
0

You can also use an extender so it's easy to reuse them for more observables:

ko.extenders.boolForEditing = function (target, allowNull) {
    var result = ko.computed({
        read: function () {
            var current = target();
            var newValue = null;
            if (current === undefined || current === null || current === '') {
                if (!allowNull) {
                    newValue = 'false';
                }
            } else {
                newValue = current ? 'true' : 'false';
            }
            return newValue;
        },
        write: function (newValue) {
            var current = target();
            var valueToWrite = null;
            if (newValue === undefined || newValue === null || newValue === '') {
                if (!allowNull) {
                    valueToWrite = false;
                }
            } else {
                valueToWrite = newValue === 'true';
            }
            // only write if it changed
            if (valueToWrite !== current) {
                target(valueToWrite);
            } else {
                if (newValue !== current) {
                    target.notifySubscribers(valueToWrite);
                }
            }
        }
    }).extend({
        notify: 'always'
    });

    result(target());

    return result;
};

Then use it like this:

this.IsMale.forEditing = this.IsMale.extend({boolForEditing: true});

The parameter provided to boolForEditing indicates whether the value may be null.

See http://jsfiddle.net/G8qs9/1/

Revolution answered 6/1, 2014 at 12:33 Comment(0)
T
0

After doing lot of research for older version of knockout prior to 3.0 there are possibly two best options

Create a knockout extender like

ko.extenders["booleanValue"] = function (target) {
    target.formattedValue = ko.computed({
        read: function () {
            if (target() === true) return "True";
            else if (target() === false) return "False";
        },
        write: function (newValue) {
            if (newValue) {
                if (newValue === "False") target(false);
                else if (newValue === "True") target(true);
            }
        }
    });

    target.formattedValue(target());
    return target;
};

To use the extender on your model, you’d do something like the following:

function Order() {
  this.wantsFries= ko.observable(false).extend({ booleanValue: null });
}

<span>Do you want fries with that?</span>
<label>
  <input type="radio" name="question" value="True"
             data-bind="value: wantsFries.formattedValue" /> Yes
</label>
<label>
  <input type="radio" name="question" value="False"
             data-bind="value: wantsFries.formattedValue" /> No
</label>

source:http://www.timlabonne.com/2013/02/building-a-knockout-js-extender-for-boolean-values/

Tabber answered 30/6, 2014 at 9:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.