We are using Knockout and jQuery in an application where we need to support IE 8 (among several other browsers). We have a case where we need to use a Knockout click binding for one element and a jQuery click handler for another element. However, we have found that these two styles of click handlers conflict with each other in IE 8 (they work fine together in all of our other supported browsers).
We have distilled the problem down to a simple reproducible example. If you don't have IE 8 on your machine, you can test this out in a newer version of IE by selecting 'Browser Mode: IE8":
With that setting in place, you can view the conflict in this jsbin. This code is:
<html>
<head>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js"></script>
<script type="text/javascript">
$(document).ready(function () {
ko.applyBindings(function () {
var viewModel = new Object();
viewModel.CanViewPage = true;
viewModel.alertA = function () {
alert("A");
};
$('#B').click(function () {
alert("B");
});
return viewModel;
}());
});
</script>
</head>
<body>
<!-- ko if: CanViewPage -->
<span data-bind="click: alertA">A</span>
<span id="B">B</span>
<!-- /ko -->
</body>
</html>
When you click on "A", the expected alert is shown. However, when you click on "B", the alert("A")
is erroneously called:
We have found two "solutions" that fix this problem, but neither one of them are viable for our application. The first way to get these two click handlers to both work in IE 8, is to remove the ko if: CanViewPage
. This can be seen in this jsbin. This "solution" is not viable because it is a core page-view permissions mechanism that we use in this large application on many pages.
The second "solution" that we found was to reverse the order of the script tags which import jQuery and Knockout. This can be seen in this jsbin. This "solution" is also not viable because these imports are controlled via a master page wrapper - changing that would require a massive QA effort to retest the entire application for bugs.
Note that since we must support IE8, jQuery 2.x or newer is not an option.
Does anyone have another solution given these constraints? Can anyone explain why this is happening?
As you've noted, the combination of setting jQuery event handlers and the if
binding doesn't work right. Knockout generally works best if all interaction with the DOM goes through Knockout. For example, if I expand your example so that CanViewPage
is initially false
but becomes true
later, the event handler for "B" disappears. http://jsbin.com/uyUTuSa/16/edit
Since you've stated that you can't use Knockout for this event handler, Bradley's solution is a good workaround. Another solution that you could use if you know that CanViewPage
will never change (isn't observable) is a custom binding that simply clears its contents on a false value and doesn't do any copying or cleaning.
ko.bindingHandlers.ifLight = {
init: function(element, valueAccessor) {
if (!valueAccessor()) {
ko.virtualElements.emptyNode(element);
}
}
};
ko.virtualElements.allowedBindings.ifLight = true;
http://jsbin.com/uyUTuSa/18/edit
jQuery keeps track of which event handler to call for an element by setting an internal property on the element. This property is assigned a unique number which is used to look up the handler in some sort of internal data storage.
The Knockout if
binding first saves a copy of the content elements using cloneNode
and then calls ko.cleanNode
on the copy, which also calls jQuery.cleanData
. This removes any data and event handlers from the copy.
Normally this all works fine, but in Internet Explorer 8 (and below), "expando" properties are converted to element attributes and are copied when using cloneNode
. When jQuery cleans the copied node, the event handler data is also removed from the original node.
When the click handler is added by Knockout for the "A" element, it uses jQuery to register the handler. jQuery appears to re-use the data (and number) from cleaned elements, so the "A" element gets the same number as the "B" element.
A solution for you is to change
$('#B').click(function () {
alert("B");
});
to
$(document).on('click', '#B',function () {
alert("B");
});
See this jsbin.
I do not know exactly why this is happening, but I suspect that the knockout "if" is causing your element to lose its click event when the ko.applyBindings() is applied.
If that is the case, the reason this works is because you are now applying the event on a parent element which is not being destroyed.
For more information on jQuery event handlers, see this excellent article : http://www.elijahmanor.com/differences-between-jquery-bind-vs-live-vs-delegate-vs-on/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With