javascripthtmlknockout.js

Knockout javascript radiobutton setting value programmatically


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>


Solution

  • 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>