Delegating the focus and blur events

Nowadays many JavaScripters are aware of the advantages of event delegation. Chris Heilmann and Dan Webb, among others, have discussed its advantages, and I've been using it as much as possible for about two years now.

Event delegation is especially useful in effects like dropdown menus, where lots of events on links may take place that can easily be handled at the root level (an <ol> or <ul> in this case).

There used to be one problem, though: although event delegation works fine for the mouse events, it does not work for the focus and blur events we need to make dropdown menus keyboard-accessible.

In the course of my ongoing event research, however, I found a way to delegate the focus and blur events, too. Maybe one of those frightfully clever JavaScript library authors will use this technique to shave off a few milliseconds of computing time

For all I know they're already aware of this technique; but it was new to me so I publish it anyway.

Example.

What is event delegation?

For those unaware of it, let's briefly discuss event delegation. As context I'll use a dropdown menu.

When a user uses a mouse to steer through a dropdown menu, you want to capture all mouseover and mouseout events in order to see whether the latest user action should open or close a menu. (You also have to carefully distinguish between useful and useless events because Firefox, Safari and Opera still don't support the mouseenter and mouseleave events, but that's another story.)

The mouseover and mouseout events bubble up; that is, when a mouseover event on a link fires, the event "climbs" the DOM tree to see if any mouseover event handlers are defined on the ancestor nodes of the link. It first checks the link itself, then goes on to the containing <li>, followed by the <ol>, and all the way up to document or window level.

That means that it's perfectly possible to define your generic onmouseover and onmouseout event handlers on the <ol> that forms the root element of the dropdown menu. When these events take place lower in the tree, event bubbling makes sure they eventually end up at the root element and can be handled there.

<ol id="dropdown">
	<li><a href="#">List item 1</a>
		<ol>
			<li><a href="#">List item 1.1</a></li>
			<li><a href="#">List item 1.2</a></li>
			<li><a href="#">List item 1.3</a></li>
		</ol>
	</li>
	[etc.]
</ol>

$('dropdown').onmouseover = handleMouseOver;
$('dropdown').onmouseout = handleMouseOut;

The great advantage of this trick is that you don't need to set two event handlers on every link, which may save some browser memory.

The focus problem

This works fine. However, as soon as you want to make your dropdown keyboard-accessible, too, you run into a problem.

In theory, making a dropdown keyboard-accessible is easy: you just define event handlers for the focus and blur events, too. The problem is that these events do not bubble up. A focus or blur event on a link fires only on the link itself, and not on any ancestor element of the link.

This is an ancient rule. A few events, most motably focus, blur, and change, do not bubble up the document tree. The exact reasons for this have been lost in the mist of history, but part of the cause is that these events just don't make sense on some elements. The user cannot focus on or change a random paragraph in any way, and therefore these events are just not available on these HTML elements. In addition, they do not bubble up.

Take the following situation:

<p id="testParagraph">
	Some text.
	<input id="testInput" />
</p>

$('testParagraph').onfocus = handleEventPar;
$('testInput').onfocus = handleEventInput;

When the user focuses on the input field, the handleEventInput function is executed. However, the event does not bubble up, so handleEventPar is not executed. In addition, it's not possible to focus on the paragraph (except when it has a tabindex attribute), so handleEventPar is never executed.

Event capturing

Except when you use event capturing.

Event capturing is the reverse of event bubbling. When an event bubbles up it starts at the event target and then moves up the DOM tree. When an event is captured, it starts at the top of the DOM tree (usually the window or document object) and then moves down to the event target.

One of the most curious conclusions of my event research is that when you define event handlers in the capturing phase the browser executes any and all event handlers set on ancestors of the event target whether the given event makes sense on these elements or not.

So let's take the same example as above, but now with addEventListener in the capturing phase. (You define it on the capturing phase by setting the last argument to true.)

<p id="testParagraph">
	Some text.
	<input id="testInput" />
</p>

$('testParagraph').onfocus = handleEventPar;
$('testInput').onfocus = handleEventInput;
$('testParagraph').addEventListener('focus',handleEventPar,true);
$('testInput').addEventListener('focus',handleEventInput,true);

Now if the user focuses on the input, the event starts at the window or document level and then moves down to the input. On the way it encounters the onfocus event handler on the paragraph and executes it, even though the event does not make sense on a paragraph.

As a result, handleEventPar is executed, and then handleEventInput.

IE

Unfortunately IE does not support event capturing. However, it supports the focusin and focusout events that, contrary to focus and blur, do bubble up. If we use these events to get IE on board, we're ready to sail.

Delegating the focus and blur events

Opera (9.5b) incorrectly fires all focus and blur events twice.

Therefore the conclusion must be that onfocus and onblur event handlers may be delegated just as onmouseover and onmouseout are by registering them in the capturing phase. In order to properly delegate all events for a keyboard-accessible dropdown menu you do:

<ol id="dropdown">
	<li><a href="#">List item 1</a>
		<ol>
			<li><a href="#">List item 1.1</a></li>
			<li><a href="#">List item 1.2</a></li>
			<li><a href="#">List item 1.3</a></li>
		</ol>
	</li>
	[etc.]
</ol>

$('dropdown').onmouseover = handleMouseOver;
$('dropdown').onmouseout = handleMouseOut;
$('dropdown').onfocusin = handleMouseOver;
$('dropdown').onfocusout = handleMouseOut;
$('dropdown').addEventListener('focus',handleMouseOver,true);
$('dropdown').addEventListener('blur',handleMouseOut,true);

Now you've succesfully delegated the focus and blur events.

Example.

Further questions

The fact that the capturing phase of the event works different from the bubbling phase raises a few questions, which I might answer at a later date:

  1. Shouldn't event bubbling and event capturing work exactly the same way? That is, shouldn't both phases either execute all event handlers they find or use the old "only on sensible elements" rule?
  2. Isn't it time to allow all events to bubble up?

(Note: In the case of focus and blur, it might make sense for the events not to bubble up. After all, if a user focuses on a link or form field, you don't want any generic window.onfocus to fire, because on the window that event has a totally different function: it fires when the user focuses on the entire browser window.)

This is the blog of Peter-Paul Koch, mobile platform strategist, consultant, and trainer. You can also follow him on Twitter.
Atom RSS

I’m around at the following conferences:

(Data from Lanyrd)

Categories:

Monthlies:

Comments

Comments are closed.

1 Posted by Dean Edwards on 14 April 2008 | Permalink

You can use focusin/focusout on MSIE to get the same effect.

2 Posted by Ivan Zhekov on 14 April 2008 | Permalink

Off topic -- is it just me, or $() is not defined?

3 Posted by Frederick Polgardy on 14 April 2008 | Permalink

In the example, $() is a shortcut for document.getElementById(). It's also available in the commonly used Prototype library.

4 Posted by ppk on 14 April 2008 | Permalink

Cool Dean, thanks for the tip. Focus/blur event delegation is now supported by all browsers. I've also added these events to the Great Event Project.

5 Posted by Victor Morales on 15 April 2008 | Permalink

I have used this trick before; My goal was to implement cross-browser event delegation for form elements (Think about a form where you add or remove fields dynamically, and you don't want deal with the hassle of registering/deregistering their event handlers)

Unfortunately, as your Event compatibility shows the focus and blur are only defined for text elements in Safari:

http://www.quirksmode.org/js/events_compinfo.html#t00

6 Posted by Sam on 17 April 2008 | Permalink

I think an even greater advantage of using event delegation (the way I understand it works) as opposed to observing events on sub-elements, should not just be that it "may save some browser memory". But that you can add elements to the container and those elements will have the same functionality without registering a new handler manually.

7 Posted by jay on 25 April 2008 | Permalink

Event delegation for the blur event is quite buggy as fas as I have tested.

In Firefox 3(beta 5) There seems no event propagation when you blur from an text-input field.
I attached in three different tests the blur-event-handler to window, then to document and then to body, but the results were the same. No blur event.

Of and there was an other strange thing. In IE (6 on winxp) the blur event of body had no srcElement.

So it is a bit of a mess. At least that's my opinion. I've been reading your event compatibility page (of cause I did) and my conclusion for now is that you have to be very careful before implementing anything like this. I give up for now.

8 Posted by Austin Jackson on 7 May 2008 | Permalink

Is there a way to get which element triggered a bubble?

9 Posted by Diego Perini on 8 May 2008 | Permalink

PPK,
please have a look at this event manager that support delegation:

http://javascript.nwbox.com/NWEvents/delegates.html

it has been discussed last year in Peter Michaux blog, it mimics focus/blur events on all browsers I could test, including Safari and Konqueror.

My work is on Google Code:

http://code.google.com/p/nwevents/

The way I attach events to the HTML element (document.documentElement) let me avoid having to wait for some kind of "onload" event.

Anyway the user is free to declare any other element as delegate.

Event delegation has many advantages, it helps write complex cross-browser applications and events are rebound automatically.

I would be very interested in your thoughts about this.


Diego Perini

10 Posted by jay on 8 October 2008 | Permalink

http://yuiblog.com/blog/2008/10/07/onfocus-onblur/