We have already seen one situation in which event bubbling can cause problems. To show a case in which .hover() does not help our cause, we'll alter the collapsing behavior we implemented earlier.
Suppose we wish to expand
the clickable area that triggers the collapsing or expanding of the
style switcher. One way to do this is to move the event handler from the
label, <h3>, to its containing <div> element:
$(document).ready(function() {
$('#switcher').click(function() {
$('#switcher .button').toggleClass('hidden');
});
});
This alteration makes the
entire area of the style switcher clickable to toggle its visibility.
The downside is that clicking on a button also collapses the style
switcher after the style on the content has been altered. This is due to
event bubbling; the event is first handled by the buttons, then passed
up to the DOM tree until it reaches the <div id="switcher">, where our new handler is activated and hides the buttons.
To solve this problem, we need access to the event object.
This is a JavaScript construct that is passed to each element's event
handler when it is invoked. It provides information about the event,
such as where the mouse cursor was at the time of the event. It also
provides some methods that can be used to affect the progress of the
event through the DOM.
To use the event object in our handlers, we only need to add a parameter to the function:
$(document).ready(function() {
$('#switcher').click(function(event) {
$('#switcher .button').toggleClass('hidden');
});
});
Event targets
Now we have the event object available to us as the variable event within our handler. The property event.target can be helpful in controlling where
an event takes effect. This property is a part of the DOM API, but is
not implemented in all browsers; jQuery extends the event object as
necessary to provide the property in every browser. With .target, we can determine which element in the DOM was the first to receive the event—that is, in the case of a click event, the actual item clicked on. Remembering that this gives us the DOM element handling the event, we can write the following code:
$(document).ready(function() {
$('#switcher').click(function(event) {
if (event.target == this) {
$('#switcher .button').toggleClass('hidden');
}
});
});
This code ensures that the item clicked on was <div id="switcher">, not one of its sub‑elements. Now clicking on buttons will not collapse the style switcher, and clicking on the switcher's background will. However, clicking on the label, <h3>,
now does nothing, because it too is a sub‑element. Instead of placing
this check here, we can modify the behavior of the buttons to achieve
our goals.
Stopping event propagation
The event object provides the .stopPropagation() method, which can halt the bubbling process completely for the event. Like .target,
this method is a plain JavaScript feature, but cannot be safely used
across all browsers. As long as we register all of our event handlers
using jQuery, though, we can use it with impunity.
We'll remove the event.target == this check we just added, and instead add some code in our buttons' click handlers:
$(document).ready(function() {
$('#switcher .button').click(function(event) {
$('body').removeClass();
if (this.id == 'switcher-narrow') {
$('body').addClass('narrow');
}
else if (this.id == 'switcher-large') {
$('body').addClass('large');
}
$('#switcher .button').removeClass('selected');
$(this).addClass('selected');
event.stopPropagation();
});
});
As before, we need to add a parameter to the function we're using as the click handler, so we have access to the event object. Then we simply call event.stopPropagation()
to prevent any other DOM element from responding to the event. Now our
click is handled by the buttons, and only the buttons; clicks anywhere
else on the style switcher will collapse or expand it.
Default actions
Were our click event handler registered on a link element (<a>) rather than a generic <div>, we would face another problem. When a user clicks on a link, the browser loads a new page. This behavior is not an event handler in the same sense as the ones we have been discussing; instead, this is the default action for a click on a link element. Similarly, when the Enter key is pressed while the user is editing a form, the submit event is triggered on the form, but then the form submission actually occurs after this.
If these default actions are undesired, calling .stopPropagation() on the event will not help. These actions occur nowhere in the normal flow of event propagation. Instead, the .preventDefault() method will serve to stop the event in its tracks before the default action is triggered.
Calling .preventDefault()
is often useful after we have done some tests on the environment of the
event. For example, during a form submission we might wish to check
that required fields are filled in, and prevent the default action only
if they are not.
Event propagation and
default actions are independent mechanisms; either can be stopped while
the other still occurs. If we wish to halt both, we can return false from our event handler, which is a shortcut for calling both .stopPropagation() and .preventDefault() on the event.
Event delegation
Event bubbling isn't always
a hindrance; we can often use it to great benefit. One great technique
that exploits bubbling is called event delegation. With it, we can use an event handler on a single element to do the work of many.
In jQuery 1.3, a new pair of methods, .live() and .die(), have been introduced. These methods perform the same tasks as .bind() and .unbind(),
but behind the scenes they use event delegation to gain the benefits
we’ll describe in this section.
In our example, there are just three <div class="button"> elements that have attached click
handlers. But what if there were many? This is more common than one
might think. Consider, for example, a large table of information in
which each row has an interactive item requiring a click handler. Implicit iteration makes assigning all of these click
handlers easy, but performance can suffer because of the looping being
done internally to jQuery, and because of the memory footprint of
maintaining all the handlers.
In our example, there are just three <div class="button"> elements that have attached click
handlers. But what if there were many? This is more common than one
might think. Consider, for example, a large table of information in
which each row has an interactive item requiring a click handler. Implicit iteration makes assigning all of these click
handlers easy, but performance can suffer because of the looping being
done internally to jQuery, and because of the memory footprint of
maintaining all the handlers.
Instead, we can assign a single click handler to an ancestor element in the DOM. An uninterrupted click event will eventually reach the ancestor due to event bubbling, and we can do our work there.
As an example, let's apply
this technique to our style switcher (even though the number of items
does not demand the approach). As seen above, we can use the event.target property to check what element was under the mouse cursor when the click occurred.
$(document).ready(function() {
event objectevent delegation, example$('#switcher').click(function(event) {
if ($(event.target).is('.button')) {
$('body').removeClass();
if (event.target.id == 'switcher-narrow') {
$('body').addClass('narrow');
}
else if (event.target.id == 'switcher-large') {
$('body').addClass('large');
}
$('#switcher .button').removeClass('selected');
$(event.target).addClass('selected');
event.stopPropagation();
}
});
});
We've used a new method here, called .is(). This method accepts the selector expressions , and tests the current jQuery
object against the selector. If at least one element in the set is
matched by the selector, .is() returns true. In this case, $(event.target).is('.button') asks whether the element clicked has a class of button assigned to it. If so, we proceed with the code from before, with one significant alteration: the keyword this now refers to <div id="switcher">, so every time we are interested in the clicked button we must now refer to it with event.target.
We can also test for the presence of a class on an element with a shortcut method, .hasClass(). The .is() method is more flexible, however, and can test any selector expression.
We have an unintentional
side-effect from this code, however. When a button is clicked now, the
switcher collapses, as it did before we added the call to .stopPropagation().
The handler for the switcher visibility toggle is now bound to the same
element as the handler for the buttons, so halting the event bubbling
does not stop the toggle from being triggered. To sidestep this issue,
we can remove the .stopPropagation() call and instead add another .is() test:
$(document).ready(function() {
$('#switcher').click(function(event) {
if (!$(event.target).is('.button')) {
$('#switcher .button').toggleClass('hidden');
}
});
});
$(document).ready(function() {
$('#switcher').click(function(event) {
if ($(event.target).is('.button')) {
$('body').removeClass();
if (event.target.id == 'switcher-narrow') {
$('body').addClass('narrow');
}
else if (event.target.id == 'switcher-large') {
$('body').addClass('large');
}
$('#switcher .button').removeClass('selected');
$(event.target).addClass('selected');
}
});
});
This
example is a bit overcomplicated for its size, but as the number of
elements with event handlers increases, event delegation is the right
technique to use.