Blog

Unclickable links using bubbling only

Bubble through a wormhole to short-circuit your JS logic.

While implementing Gravity Department’s typeahead library an issue was discovered that affects all versions of Internet Explorer:

You can delegate a click event for a link which will bubble to the document and trigger a callback — which can destroy the link (and its default behavior) — all before the link itself receives the click event.

Strangely, the quirk manifests due to the DOM structure and not behavior changes using e.preventDefault() or e.stopPropagation().

The normal flow

This event delegation flow is consistent across browsers:

  1. Click a link.
  2. The event originates on the <a> triggering the default behavior (browser navigates to the href).
  3. The event bubbles up the DOM to the document triggering the delegated event.
  4. The callback replaces the inner HTML of .wrap with the message.
  5. End ➔ the browser redirected.
<div class="wrap">
    <a href="http://aaa.com">AAA</a>
    <a href="http://bbb.com">BBB</a>
    <a href="http://ccc.com">CCC</a>
</div>
jQuery(document).on('click', 'a', function (e) {
    jQuery('.wrap').html('Loading');
});

Short-circuit the default event with a bubbling wormhole

A small HTML change creates a race condition between default events and delegated events in IE. See the <div> inside the link?

<div class="wrap">
    <a href="http://aaa.com"><div>AAA</div></a>
    <a href="http://bbb.com"><div>BBB</div></a>
    <a href="http://ccc.com"><div>CCC</div></a>
</div>

That creates a wormhole. Now the callback will execute before the link’s default behavior and prevent it from happening.

  1. Click a link.
  2. The event originates on the <div> inside the <a> and starts bubbling up.
  3. Bubbling reaches the <a> and eventually document, but something unexpected happens.
  4. Our delegated event executes before the click registers on the <a>.
  5. The callback replaces the inner HTML of .wrap with the message.
  6. The DOM containing the anchors is destroyed (so is the default link behavior).
  7. End ➔ the browser never redirects.

It makes no sense, but IE will bubble clicks on the child element to its furthest ancestor faster than its parent <a> can execute default behavior.

Technically this is our fault for trusting the browser to execute default and delegated events synchronously. They are asynchronous. Bubbling can wormhole the space/time continuum.

Affected browsers

IE11 and below still have this problem.

Functional browsers

Microsoft Edge and other modern browsers are either “friendly” to programmers, or coincidentally don’t exhibit the race condition (for now).

Best practice

You can handle the async nature of events. Always cancel then reproduce default behavior when implementing delegated events, so you can direction the execution order.

jQuery(document).on('click', 'a', function (e) {
    // Cancel the default behavior
    e.preventDefault();

    // Manually replicate the default behavior
    var href = jQuery(this).attr('href');

    if (typeof href !== 'undefined' && href !== '') {
        window.location = href;
    }

    // Replace the DOM with message
    jQuery('.links').html('Loading');
});

Discourse Gravitated