I have a custom control with the following code. I expect "Option 1" to be selected on page reload. But nothing is selected. But I can manually select either options.
Initially the output is
Element value = on, Observable value = abc
But after manually selecting the radio button,
Change event: Setting observable to abc
Change event: Setting observable to def
Update: Element value = abc, Observable value = def
Update: Element value = def, Observable value = def
Change event: Setting observable to def
Change event: Setting observable to abc
Update: Element value = abc, Observable value = abc
Change event: Setting observable to abc
Update: Element value = def, Observable value = abc
What is wrong is setting the initial value of the radio button?
this.tempitems = ko.observableArray([{
guid: "abc",
name: "Option 1"
},
{
guid: "def",
name: "Option 2"
}
]);
this.tempitem = ko.observable("def"); // Initially selects "Option 2"
this.tempitem = ko.observable("abc");
console.log("Initial value of tempitem:", ko.unwrap(self.tempitem));
ko.bindingHandlers.rbChecked = {
init: function(element, valueAccessor, allBindingsAccessor,
viewModel, bindingContext) {
var value = valueAccessor();
// Handle the change event
ko.utils.registerEventHandler(element, "change", function() {
console.log(`Change event: Setting observable to ${element.value}`);
value(element.value); // Update the observable
});
// Set the initial checked state
if (ko.unwrap(value) === element.value) {
console.log("Init: Setting element.checked = true");
element.checked = true;
}
var newValueAccessor = function() {
return {
change: function() {
value(element.value);
}
}
};
ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, viewModel, bindingContext);
if (value != undefined && $(element).val() == value()) {
setTimeout(function() {
var toggle = $(element).closest('.btn-toggle');
$(toggle).children('.btn').removeClass('active');
$(toggle).children('.btn').removeClass('btn-primary');
$(toggle).children('.btn').addClass('btn-default');
var btn = $(element).closest('.btn');
$(btn).addClass('btn-primary');
$(btn).addClass('active');
}, 0);
}
},
update: function(element, valueAccessor, allBindingsAccessor,
viewModel, bindingContext) {
const value = ko.unwrap(valueAccessor());
console.log(`Update: Element value = ${element.value}, Observable value = ${value}`);
// Ensure the checked state reflects the observable value
element.checked = value === element.value;
// If the radio button is checked, trigger the change event to update the observable
if (element.checked) {
element.dispatchEvent(new Event("change", {
bubbles: true
}));
}
if ($(element).val() == ko.unwrap(valueAccessor())) {
setTimeout(function() {
$(element).closest('.btn').toggleClass('active');
var toggle = $(element).closest('.btn-toggle');
$(toggle).children('.btn').removeClass('active');
$(toggle).children('.btn').removeClass('btn-primary');
$(toggle).children('.btn').addClass('btn-default');
var btn = $(element).closest('.btn');
$(btn).addClass('btn-primary');
$(btn).addClass('active');
}, 0);
}
}
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"></script>
<ul class="list-group" data-bind="foreach: tempitems">
<li>
<input class="form-checkbox"
type="radio"
name="target"
data-bind="attr: { 'id': guid }, rbChecked: $parent.tempitem, value: guid" />
<span data-bind="text: name"></span>
<span data-bind="text: $parent.tempitem"></span>
</li>
</ul>
Only initialise tempitem once with the desired initial value and avoid feedback loops by only updating the observable when the user interacts with the radio button. Don't dispatch change events in the update method.
Let Knockout handle the synchronization of the checked state instead of directly manipulating element.checked.
Here the rbChecked binding uses Knockout for the two-way binding to avoid unnecessary DOM access.
I also had to data-bind the value so it is "abc" instead of "on"
ko.bindingHandlers.rbChecked = {
init: function(element, valueAccessor) {
const value = valueAccessor();
// Change event is triggered by user interaction
ko.utils.registerEventHandler(element, "change", function() {
console.log(`Change event: Setting observable to ${element.value}`);
value(element.value); // Update the observable
});
if (ko.unwrap(value) === element.value) {
console.log(`Init: Setting element.checked = true for value ${element.value}`);
element.checked = true;
}
},
update: function(element, valueAccessor) {
const value = ko.unwrap(valueAccessor());
console.log(`Update: Element value = ${element.value}, Observable value = ${value}`);
// Synchronize the checked state with the observable
element.checked = value === element.value;
},
};
// ViewModel
function ViewModel() {
const self = this;
self.tempitems = ko.observableArray([{
guid: "abc",
name: "Option 1"
},
{
guid: "def",
name: "Option 2"
},
]);
self.tempitem = ko.observable("abc"); // Initially selects "Option 1"
console.log("Initial value of tempitem:", ko.unwrap(self.tempitem));
}
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"></script>
<ul class="list-group" data-bind="foreach: tempitems">
<li>
<input
class="form-checkbox"
type="radio"
name="target"
data-bind="attr: { id: guid, value: guid }, rbChecked: $parent.tempitem"
/>
<span data-bind="text: name"></span>
</li>
</ul>
<p>Selected Value: <span data-bind="text: tempitem"></span></p>