Click event delegation on the iPhone

From the dawn of history browsers have supported event delegation. If you click on an element, the event will bubble all the way up to the document in search of event handlers to execute.

It turns out that Safari on the iPhone does not support event delegation for click events, unless the click takes place on a link or input. That’s an annoying bug, but fortunately there’s a workaround available.

Event delegation

Let’s recap briefly. Event delegation is the technique of attaching event handlers not to the elements you actually want to read out events from, but to a higher-level element.

For instance, say you have a dropdown menu with a lot of menu items. You need mouseover and mouseout events to get the menu to work (not to mention focus and blur to make it keyboard-accessible, too).

Now you could attach a mouseover and mouseout event to every single menu item, but it’s far more efficient to attach a single mouseover and mouseout event handler to the topmost element of the dropdown menu; likely a <ul>.

<ul id="dropdown">
	<li>Item one
		<ul>
			<li>Subitem one</li>
			<li>Subitem one</li>
			<li>Subitem one</li>
		</ul>
	</li>
	... etc ...
</ul>

$('dropdown').onmouseover = openMenu;
$('dropdown').onmouseout = closeMenu;

The trick here is event bubbling. If the user mouses over an <li>, the mouseover event bubbles up all the way to the document. That is, it first checks of the <li> itself has an onmouseover event handler, then the parent of the <li>, then its parent, etc. etc. all the way to the document. Any event handler that’s found is executed.

Thus the event bubbles up to the <ul id="dropdown">, encounters an onmouseover event handler there, and executes it. This allows you to capture all mouseover and mouseout events in the dropdown menu with only one event handler for each event, which saves memory (not to mention lines and lines of code).

The iPhone bug

On the iPhone, event delegation does not work for the click event. It works fine for mouseover and mouseout, fine for the touch events, but not for click.

However, as Mark Haylock showed me, event delegation does work if the target of the event is a link or an input field.

Still, Safari has returned to the Dark Age. Netscape 3 did the same: it only captured click events that took place on links and form fields, and ignored the rest. Every single browser that was released after 1997 supported full event delegation, though.

Except, apparently, for the iPhone.

This test page contains an extremely simple script that sets an onclick event handler on the document and then waits for the user to click on the bordered div. Once that happens, the div is replaced by another div.

The second test page broadens that experiment a little: you can now also click on links and inputs. Those click events bubble up fine on the iPhone, but it ignores any click event starting on another element.

Let’s not mince words: this is a bug. Safari does not support something that has been supported since 1998 by absolutely every browser ever released.

Why doesn’t it work?

That said, I’m certain that the nifty Apple engineers did not make a mistake. There must be a reason for this behaviour. I don’t know what that reason is, but currently I think that it’s a memory management problem. Apparently, making all elements on a page clickable demands too many resources, and the Apple engineers decided to disable it.

I’m guessing here; maybe the problem is something else entirely. (Especially since one could argue that all other events would have the same memory problem, but event delegation works fine for them.)

If you know why event delegation has been disabled for the click event, please leave a comment.

The workaround

This is a serious problem for web pages that have a lot of clickable elements but where the programmer has wisely decided to handle all those click events centrally in one event handler.

Fortunately it’s pretty easy to solve: you have to make the element clickable by giving it an onclick event handler of its very own. That handler can be empty; as long as it’s present it will make any element clickable.

This works even on the iPhone:

document.onclick = function () {
	// change div
}
div.onclick = function () {}

We still handle the event on the document level, but we add an empty event handler to the div we want to click. Now all of a sudden the div is clickable, the event bubbles up to the document and is handled correctly.

The only trick here is that we have to repeat this every time we change the div. Once the old one is destroyed and a new one appended, the onclick event handler is gone, too, and we need to set a new one:

document.onclick = function () {
	var newDiv = document.createElement('div');
	// populate div
	newDiv.onclick = function () {};
	document.body.appendChild(newDiv);
}
div.onclick = function () {}

Apart from its ugliness, I see one problem with this approach: if my guess is right and the Apple engineers have in fact disabled click event delegation because of memory considerations, this workaround will create new memory problems. Lots of them if you have lots of divs that must become clickable.

But that can’t be helped. Event delegation simply must work for the click event, and if we have to brute-force it at the expense of memory that’s just too bad for Safari iPhone.

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 Steve Webster on 28 September 2010 | Permalink

The reason for this deviation may be that users are tapping on random elements all the time on a touch device like the iPhone, and not every tap is intended to be a click. The overhead of generating and then propagating the click events through the capture/target/bubble phases could be significant on a mobile device when you're already using a bunch of precious memory just rendering the page.

I'd prefer that Apple generated and propagated events as per the spec, but with my pragmatist hat on I can see why they may have made this deviation.

Incidentally, this also works:

div.addEventListener('click', function() {}, false );

Functionally it's no better, but it does feel a little less icky than using onclick.

2 Posted by Sean Hogan on 28 September 2010 | Permalink

What happens when you register listeners in the capture phase. e.g.

document.addEventListener("click", function(e) {}, true);

3 Posted by Sean Hogan on 28 September 2010 | Permalink

Also, does mousedown bubble? This would facilitate a work-around of adding element.onclick in the preceding mousedown event.

4 Posted by Aeron on 28 September 2010 | Permalink

What about extending the Element prototype to always include the onclick handler?

5 Posted by Miller Medeiros on 28 September 2010 | Permalink

I'm not sure why but I got it to work on iPhone 3GS iOS4 and iPad: http://github.com/millermedeiros/zepto/blob/master/dev/test/events.html

I'm going to trim down the test file and attach the event listener without using the framework but the way I'm doing the delegation and event binding is really simple, shouldn't affect anything, I'll keep you updated.

Cheers!

6 Posted by Miller Medeiros on 28 September 2010 | Permalink

confirmed, your example doesn't work and my tests works (even removing the framework code). See: http://labs.millermedeiros.com/js/ios_delegate/01.html

After spending a couple minutes trying to figure out why that happens I discovered the click events are indeed bubbled up the DOM tree, but they simply never reach the BODY element.

If the div you are attaching the listener is at least 1 level up the tree (e.g. a #wrapper div that contains the whole document content) the delegation works just fine.

PS: you should always try to delegate events to the closest node to avoid unnecessary bubbling, attaching to the body is better than having multiple listeners but should be slower than attaching to a parent element that is closer in the tree.

I was really curious to find out the reason since I got it working properly a couple days ago by coincidence and never experienced the problem before...

7 Posted by Stefan Liden on 28 September 2010 | Permalink

I thought Steve might be right until Miller wrote that it actually bubbles until it reaches the body, very weird indeed.

@Miller: I've read the statement "you should always try to delegate events to the closest node", but I've not actually seen anybody discuss bubble performance, and even in deeply nested DOM I find that an event bubble to the document.body almost instantaneously (less than 1 ms). In my own simple tests I have not been able to produce any differences large enough to warrant the above claim. Are you aware of any discussions that you could point me to, or do you maybe have personal experiences on the subject that you could share?

8 Posted by Miller Medeiros on 29 September 2010 | Permalink

My comment about attaching the event listener to "the closest node possible" was just an "assumption" and is something that I don't even think it's possible to test properly or that makes any difference at all (I should not have said that), I've talked about performance but in fact the most important thing is to avoid that other listeners would stop the propagation by mistake before it reaches the document and also to call the handler before other handlers attached to parent nodes...

I don't know how the event bubbling is implemented by the browsers but the way I would do it is to check if the event needs to be bubbled up and/or triggered before actually doing it and would stop the propagation as soon as possible so THEORETICALLY it would be a little bit faster...

PS: on Chrome the click event usually takes 0ms to bubble up to the `document`, it doesn't matter how many elements you have on the page, don't get crazy optimizing things that doesn't need to be optimized and focus on the important things, it was an "unhappy" comment.

9 Posted by ppk on 29 September 2010 | Permalink

@Miller: Yes, you seen to be right. If I add a div outside the test div and set the event handler there, it works fine.

This leaves me all the more confused. Why not allow the events to bubble up to the body and the document? I'm not sure any more if the reason for denying this is memory management, but what else can it be?

As to setting the handlers on the closest element, that's not a general rule and, as Stefan said, there's no research to support it.

You can set event handlers anywhere you like, and the events will bubble up. Except to the body or document on the iPhone.

10 Posted by Jimmy Byrum on 30 September 2010 | Permalink

Setting the cursor of the div to pointer in CSS also does the trick without any extra JS.

I've recreated the first test case with this addition here:

http://jimmybyrum.com/tests/eventdelegation.html

and everything works fine.

11 Posted by Jethro Larson on 27 October 2010 | Permalink

Good catch ppk. Thanks, I'll have to watch out for this.