Event Bubble & Capture Phases

One of the less understood, but powerful feature of browser events are their phases. According to the W3C level 2 spec there are three phases[1]: AT_TARGET=2, BUBBLING_PHASE=3, and CAPTURING_PHASE=1. Most browsers also implement a fourth phase[2]: NONE=0.

Getting ready

Just a quick note that everything discussed in this article is for modern browsers (all browsers except IE <9). Prior to IE 9, Internet Explorer used its own event system, instead of conforming to the W3C spec. Additionally, while we may show some JavaScript for attaching listeners and stopping events, it is important to know that JavaScript is a separate system from browser events.

To understand the capture and bubble phase, assume we have two elements element2 inside of element1. We assign a click event to element1 and click on element2. The DOM would look something like:

-----------------------------------
| element1                        |
|   -------------------------     |
|   |element2               |     |
|   -------------------------     |
|                                 |
-----------------------------------

Now the capture phase is when the browser searches the DOM starting at the root node (usually DefaultView), until it reaches the node where the event was triggered. The capture phase would look something like:

----------------|  |---------------
| element1      |  |              | <-- event assigned node
|   ------------|  |---------     |
|   |element2    \/         |     | <-- triggering node
|   -------------------------     |
|                                 |
-----------------------------------

Now the bubble phase is when the browser searches the DOM starting at the node where the event was triggered, until it reaches the root node. The bubble phase would look something like:

---------------- /\ ---------------
| element1      |  |              | <-- event assigned node
|   ------------|  |---------     |
|   |element2   |  |        |     | <-- triggering node
|   -------------------------     |
|                                 |
-----------------------------------

Putting both phases together for the W3C model, you get:

---------------|  |-- /\ ----------
| element1     |  |  |  |         | <-- event assigned node
|   -----------|  |--|  |---------|
|   |element2   \/   |  |         | <-- triggering node
|   ------------------------------|
|                                 |
-----------------------------------

The target phase happens between the capture and bubble phases.

How do it…

When attaching an event, the browser defaults to the bubble phase (if the event supports bubble):

document.getElementById('element1').addEventListener('click', function(event) {
    // handle bubble event
});

To attach an event to the capture phase, set the third, optional argument of addEventListener to true:

document.getElementById('element1').addEventListener('click', function(event) {
    // handle capture event
}, true);

Besides event phases, some events also have default actions, such as follow the href of an anchor tag on click. These default actions may occur before or after the DOM event phase cycle and may be prevented using (even ones that occur before the cycle):

document.getElementById('element1').addEventListener('click', function(event) {
    event.preventDefault();
});

Lastly, to stop the event system from completing the phase cycle, call:

document.getElementById('element1').addEventListener('click', function(event) {
    event.stopPropagation();
});

This will end the current phase and all subsequent phases.

Here is a simple demo that attaches two events (one to bubble and the other the the capture phase), and prints the target, targetElement, and phase, to the console:

See the Pen Fun With Event Phases by Matt Snider (@mattsnider) on CodePen.


How it works…

Before discussing the event cycle, lets summarize the steps that happen when the user or browser triggers an event:

  1. Event interface instance created
  2. Put event onto the queue (developer events skip this step)
  3. Event loop processes the event
  4. DOM path to triggering element set
  5. Default action (if applicable)
  6. Capture phase (can be skipped, CAPTURE_PHASE=1)
  7. Target phase (can be skipped, AT_TARGET=2)
  8. Bubble phase (can be skipped and if applicable, BUBBLING_PHASE=3)
  9. Default action (if applicable)

The event is triggered and its instance is created and added to the event queue. There is a single event loop per DOM that pulls the next events off the queue. The browser then calculates the path from the root element to the triggering element. The event may trigger a default action next or after the event phase cycle, and these actions may be prevented during the event phase cycle using event.preventDefault() (not all events may be prevented, such as unload). The developer can check booleans to see if the event is cancellable using event.cancelable, or if it was cancelled using event.defaultPrevented. Then the browser begins the capture, then target, and bubble phases, triggering any necessary callbacks. The phase cycle my be short-circuited at anytime by calling event.stopPropagation(). Lastly, there are events that don’t bubble (such as blur, focus, or scroll). The developer can check this by looking at the boolean event.bubbles. Events that don’t bubble only execute the first two phases.

Calling either event.stopPropagation() or event.stopImmediatePropagation() will cause the the remainder of the capture, target, and bubble phases to stop, triggering the immediate execution of the default action (if one exists for the event). So, when called during the capture phase, the rest of that phase and the other two phases will be skipped, but when called during the bubble phase, just the remaining elements in the bubble phase are skipped. The difference between the two, is event.stopPropagation() affects only the flow for the current event listener, while event.stopImmediatePropagation() will stop propagating for all other event listeners as well.

The event interface passed into callback functions will have two useful DOM pointers, event.target and event.currentTarget. The event.target is the element that the event was assigned to and event.currentTarget is the element currently pointed to by the event phase. When event.target === event.currentTarget you will be in the phase AT_TARGET=2, otherwise you should be in either CAPTURE_PHASE=1 or BUBBLING_PHASE=3 phases.

If a default action triggers before the event phase cycle, and is prevented during the event phase cycle, the user may see weird behavior. For example, a checkbox will trigger a default action that checks the box before the event phase cycle, and if the developer prevents this behavior during the event phase cycle, then it will become unchecked. There is nothing the developer can do to prevent this, so it is important to know what events have default actions and when those actions execute.

Lastly, the default action of some events is to trigger other event, such as hitting the enter key while in an input element inside a form to submit the form, but that is a discussion for another article.

References

  1. W3C Events Interface Spec
  2. MDN - event.eventPhase
  3. JavaScript Event order

Passing Objects into addEventListener Instead of Functions

I was reviewing the browser event stack the other day and was reminded of a rarely used feature of addEventListener that allows developers to autobind the execution context object, instead of requiring a call to bind or using a library, that is worth sharing, if you weren’t already aware.

How do it…

Typically, when attaching an event, we write:
 var myObj = { handleEvent: function (evt) { // 'this' will be scoped ...

Running Android Tests on a Device or Emulator

I have not been doing much web development lately, so its been difficult to come with interesting topics. However, I have been doing a lot of android development and since many engineers have to work cross discipline, I think an android article be relevant. This article will discuss how to run unit tests against your android code, directly on the android device or emulator.

Getting ready

You will need to install the

Cherry-Picking for Fun and Profit

In Git, it is often useful to merge one specific commit from one branch into another. Frequently this happens when Using Git Interactive Rebase With Feature Branches, as you develop in the branch, you realize that one or more of your commits should be added to master right away, but not all the CLs. Enter git cherry-pick <changelist hash> for the win! The cherry pick command allows you to merge one CL from a ...

Gaming Engine - Snake Demo v2

Last week I was busy at GDC and have not had time to put together a detailed article, so in the spirit of GDC, I thought I would share the latest iteration of my HTML5 gaming engine (still very rough). There has been a lot of progress around the Game class to support stages (or levels) and a score board to track the player’s score. The stages are demoed by a new version ...

Use && Instead of Semicolon to Separate Commands

Today’s article will be short and will cover a bash topic that frustrates me to no end. Please don’t use ; to separate commands, when you mean &&. There is an important difference between the two and many developers never realize that they want to be using && in their scripts.

How do it…

Here is a common oneliner that you might use to compile a package:
 ./configure ; make ; make ...

Using Promises to Cache Static AJAX JSON Data

This article showcases a useful caching strategy for static data that is fetch via AJAX. We will use jQuery to setup a promise and cache the data in the localStorage for subsequent page loads or data loads.

Getting ready

A modern web browser supporting localStorage and JSON. Also, a basic understanding of promises[2] is helpful.

How do it…

Here is the code:
 (function($) { var oKeyDeferredMap = {}; function fnReadData(sKey) ...

Connecting to Github and EC2 Through a Proxy on Port 80 or 443

Today we’ll cover how to connect to github and EC2 through a draconian proxy allowing only port 80 and 443. Github uses SSH, so like EC2 it can be connected to using SSH tunnelling. This article is based on a blog post by tachang[1], which needed some additional explanation and changes to work behind my proxy. I will be explaining how to connect on a unix-based machine, but these settings should also work on ...

Introducing Gaming Engine - Snake Demo v1

In my not so copious spare time over the past few months, I’ve been working on a game engine to power two dimensional board-based games. The engine has a long way to go, but I have reach the first demo milestone and wanted to share it with you. Here is a basic version the snake game written using the game engine. It illustrates a working main thread, responsiveness to keyboard commands, interaction between a ...

jQuery Function for Change Event and Delayed Keydown Event

In my experience, it is rare to assign only a change event to a text input, as any callback that would be executed for the change event should also be called on a key event as well, but with a slight delay (think how an autocomplete shows results as you type). This is a common pattern and I was surprised to not immediately find a jQuery plugin implementing it, so I decided to add one ...