Recently I was again struggling with the ASP.NET Update Panel killing my JavaScript interactions because it replaces the DOM elements it surrounds on every refresh. My previous solution for this was simply to rebind those events after each Update Panel refresh was completed. This works perfectly well, but it always seemed a little heavy handed to me.
So in digging into this issue a little more, I came upon a few posts that talked about using event delegation to solve this problem (thanks to everyone out there who wrote about delegation, cause you're smarter than I am). The basic idea is that you attach events at a higher level in the DOM tree and use the DOM's event bubbling mechanism to catch them. For example, let's say we have the following ASPX snippet:
<div id="container">
<ul>
<asp:UpdatePanel>
<li class="listItem">Item One</li>
<li class="listItem">Item Two</li>
<li class="listItem">Item Three</li>
<li class="listItem">Item Four</li>
</asp:UpdatePanel>
</ul>
</div>
Now let's assume that you've done this somewhere in JavaScript:
function bindLiEvents() {
$("li.listItem").bind("click", function() { alert($(this).html()); });
}
When this JavaScript is executed jQuery loops over all the found elements and individually attaches an event handler to each. So already there is an inherent performance gain to be had by not doing this looping. Furthermore there is now the problem with the Update Panel. If the user takes some action that causes the panel to refresh, then the four <li> elements are destroyed and recreated and unless I call bindLiEvents again after the refresh I loose this functionality.
Enter the event delegation concept. Why not attach my handler to a DOM element that's outside the Update Panel – the <div> element in this case. This may seem a bit counter intuitive, but if you're aware of how event bubbling works in the DOM you can see how this might work.
Event bubbling simply means that events at lower levels in the DOM tree are propagated to higher levels. For example when an <li> is clicked in our example, the click event is also fired on the <ul> and the <div> and beyond. We can use this behavior to our advantage in this instance because event bubbling is intrinsic to DOM elements – when new elements are added they automatically exhibit this behavior.
So now let's look at how we might attach an event to the <div> element:
function bindLiEvents() {
$("#container").delegate("click", "li.listItem",
function() { alert($(this).html()); });
}
Don't worry about where the delegate method comes from – it's not native to jQuery but I'll get to that. What's happening here is that I'm binding a very generic event handler to the <div> element, and telling that handler to listen for click events on elements that match the "li.listItem" selector. So now, when an event bubbles up from an <li> element the handler on the <div> fires, tests if the event originated on an <li> with a class of "listItem" and then calls the function callback.
There are advantages to using the approach. First, event handlers are attached only once, and only at a single point which saves time when doing the initial bind. Second, any new elements added to the Update Panel will automatically fire the delegated event handler – no rebind is required.
So if you want to try this out for yourself, here is a jQuery plugin for the delegate method:
(function ($) {
var bubbledEvents = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove',
'mouseover', 'mouseout', 'keydown', 'keypress', 'keyup'];
var allowedEvents = { };
$.each(bubbledEvents, function(idx,event) { allowedEvents[event] = true; });
$.fn.extend({
delegate: function (event, selector, func) {
return $(this).each(function () {
if (allowedEvents[event])
$(this).bind(event, function (e) {
var el = $(e.target), result = false;
while (!$(el).is("body")) {
if ($(el).is(selector)) {
result = func.apply($(el)[0], [e]);
if (result === false) { e.preventDefault(); }
return;
}
el = $(el).parent();
}
});
});
},
undelegate: function (event) { return $(this).each(function () {
$(this).unbind(event); }); }
});
})(jQuery);