Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Knockout.js seems to be clobbering my jQuery event handlers, how rude

Ok I've been trying to unravel this mess for a few hours now and have gotten nowhere, analogous to a dog chasing its tail. Here's the situation.

I'm using Knockout.js for my UI, which works great by itself. However, I'm trying to use some third party code that makes dropdowns and checkboxes look all pretty. Actually I'm not even sure if this is a third party library or just something our designers wrote. This code hides the real checkbox and replaces it with a fake <span /> that mimics a checkbox through CSS. The click event of the span triggers the change event of the real checkbox:

// this code updates the fake UI
this._changeEvent = function() {
    self.isChecked = self.$input.is(':checked');
    self._updateHTML(false, true);
    jQuery(self).trigger('change');
};

// when the user clicks the fake checkbox, we trigger change on the real checkbox
this.$fake.on('click', function(e) {
    e.preventDefault();
    self.$input.click().trigger('change');
});

// Bind _changeEvent to the real checkbox
this.$input.change(this._changeEvent);

This actually works with Knockout.js, since Knockout will listen to that event handler. In other words, when the user clicks the fake checkbox, the bound Knockout model gets updated. However, what does not work is updating the model. If I call:

model.SomeValue(!curValue); // SomeValue is bound to a checkbox, flip its value

The model gets updated, but the fake UI is not updated. I've traced this problem down to the code in ko.bindingHandlers.checked.update which does the following:

// When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
element.checked = value;

Basically, the element.checked property is set, but no events are fired. Thus, the _changeEvent function is never called. So, I've implemented my own ko.bindingHandlers.checked.update function, which is a copy of the built-in one. In theory, this is all I should need to do:

   ko.bindingHandlers.checked.update = function (element, valueAccessor)
   {
      var value = ko.utils.unwrapObservable(valueAccessor());

      if (element.type == "checkbox")
      {
         if (value instanceof Array)
         {
            // When bound to an array, the checkbox being checked represents its value being present in that array
            element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
         }
         else
         {
            // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
            //element.checked = value;
            $(element).prop('checked', value).trigger('change'); // <--- this should work!
         }
      }
      else if (element.type == "radio")
      {
         element.checked = (element.value == value);
      }
   };

Rather than setting element.checked, I instead call .prop('checked', value) and trigger the change event. However, this is not working. Here's what I know so far:

  1. If I remove Knockout.js from the equation, $(element).prop('checked', value).trigger('change'); works perfectly fine. So, knockout.js is screwing with the event some how. Is it unbinding that event handler?
  2. I've confirmed $(element) is the same thing as this.$input in the fake checkbox binding code. I can set other expando properties on this element, and they show up.
  3. I've tried a few approaches to try to debug into Knockout.js and jQuery to see if the event is still bound, however I haven't really gotten anywhere with this approach. My hunch is that Knockout.js somehow replaced the change event handler with its own internal one, and existing bindings were removed. I haven't found a way to confirm this yet.

My Question: Mainly, I'm looking for a solution to this problem. Does Knockout.js remove existing change events that were there before the model was applied? What are the next steps in debugging this code and figuring out exactly what's going on?

like image 262
Mike Christensen Avatar asked Aug 14 '13 18:08

Mike Christensen


People also ask

Does knockout use jQuery?

Knockout out provides a sophisticated way to use jQuery methods with your binding.

Why KnockoutJS is used?

Why Use KnockoutJS? KnockoutJS library provides an easy and clean way to handle complex data-driven interfaces. One can create self-updating UIs for Javascript objects. It is pure JavaScript Library and works with any web framework.

Is KnockoutJS a library?

Knockout is a JavaScript library that helps you to create rich, responsive display and editor user interfaces with a clean underlying data model.

Where is KnockoutJS used?

Knockout. js was used to build such popular websites as Ancestry.com, Vogue, and Microsoft Azure portal. Thanks to its MVVM model, it's perfect for creating rich and responsive user interfaces with a clean, underlying data model. Today, Knockout.


1 Answers

Rather than overriding the base checked handler, just create your own custom binding to update the UI.

Here's an example of a model and a custom binding to handle updating the UI:

var Model = function () {
    this.checked = ko.observable(false);
};

ko.bindingHandlers.customCheckbox = {
    init: function (element) {
        // Create the custom checkbox here
        $(element).customInput();
    },
    update: function (element) {
        // Update the checkbox UI after the value in the model changes
        $(element).trigger('updateState');
    }
};

I'll bind the model to the following HTML:

<input type="checkbox" name="genre" id="check-1" value="action" data-bind="checked: checked, customCheckbox: checked" />

And really that's all I'll need to do.

Here's an example: http://jsfiddle.net/badsyntax/4Cy3y/

[edit] - i think i rushed through reading your question and didn't get the crux of what you were asking. I'll leave my answer here in any case.

like image 105
badsyntax Avatar answered Oct 18 '22 04:10

badsyntax