I think this has got to be a common situation and I'm wondering if there's an accepted convention of how to handle this in Knockout. You have a "yes-no" dropdown (or pair of radio buttons), and it's default value is a blank item (or both unchecked, for the radio buttons). The user must make a choice in order to continue.
This doesn't perfectly map to a boolean value in your model, because there are actually three possible values. True, false, and no user selection. In C# you might consider using a nullable boolean, and in Java you might use java.lang.Boolean. In both cases, "null" could represent no user selection.
JavaScript doesn't have nullables, but since it does not enforce variable types, you could adopt a convention that a particular variable can be null, true, or false, and use it in a similar fashion to a nullable boolean in C# or a java.lang.Boolean.
First off, there's the issue of binding to boolean. Knockout wants all bound values to be string types by default. This is discussed here and here, and the solution provided by RP Niemeyer is to use a custom binding, like this: (Link to JS Fiddle for this example)
ko.bindingHandlers.booleanValue = {
init: function(element, valueAccessor, allBindingsAccessor) {
var observable = valueAccessor(),
interceptor = ko.computed({
read: function() {
return observable().toString();
},
write: function(newValue) {
observable(newValue === "true");
}
});
ko.applyBindingsToNode(element, { value: interceptor });
}
};
So I used this as my starting point, and I came up with this custom binding. It seems to work. I'm interested in community feedback on this approach. Checkout the jsfiddle for this to experiment with it yourself. Any drawbacks and/or scalability issues with this?
ko.bindingHandlers.nullableBooleanValue = {
init: function(element, valueAccessor, allBindingsAccessor) {
var observable = valueAccessor(),
interceptor = ko.computed({
read: function() {
console.log(observable());
console.log(typeof(observable()));
var result = null;
if(observable() === true){
result = "true";
} else if(observable() === false){
result = "false";
} else { // Default is null, which represents no user selection
result = "null";
}
console.log("transforming on read:")
console.log(typeof(observable()));
console.log(observable());
console.log(typeof(result));
console.log(result);
return result;
},
write: function(newValue) {
var result = null;
if(newValue === "true"){
result = true;
} else if(newValue === "false"){
result = false;
} else { // Default is null, which represents no user selection
result = null;
}
console.log("transforming on write:")
console.log(typeof(newValue));
console.log(newValue);
console.log(typeof(result));
console.log(result);
observable(result);
}
});
ko.applyBindingsToNode(element, { value: interceptor });
}
};
var model = {
state: ko.observable(null)
};
ko.applyBindings(model);