Click, touch, load, drag, change, input, error, resize — the list of possible DOM events is lengthy. Events can be triggered on any part of a document, whether by a user’s interaction or by the browser. They don’t just start and end in one place; they flow though the document, on a life cycle of their own. This life cycle is what makes DOM events so extensible and useful. As developers, we should understand how DOM events work, so that we can harness their potential and build engaging experiences.

Throughout my time as a front-end developer, I felt that I was never given a straight explanation of how DOM events work. My aim here is to give you a clear overview of the subject, to get you up to speed more quickly than I did.

I will introduce the basics of working with DOM events, then delve into their inner workings, explaining how we can make use of them to solve common problems.

Listening For Events

In the past, browsers have had major inconsistencies in the way they attach event listeners to DOM nodes. Libraries such as jQuery have been invaluable in abstracting away these oddities.

As we move ever closer to standardized browser environments, we can more safely use the APIs from the official specification. To keep it simple, I will describe how to manage events for the modern Web. If you are writing JavaScript for Internet Explorer (IE) 8 or below, I would advise using a polyfill or framework (such as jQuery) to manage event listeners.

In JavaScript, we can listen to events using this:

element.addEventListener(<event-name>, <callback>, <use-capture>);
  • event-name (string)
    This is the name or type of event that you would like to listen to. It could be any of the standard DOM events (clickmousedowntouchstart,transitionEnd, etc.) or even your own custom event name (we’ll touch on custom events later).
  • callback (function)
    This function gets called when the event happens. The event object, containing data about the event, is passed as the first argument.
  • use-capture (boolean)
    This declares whether the callback should be fired in the “capture” phase. (Don’t worry: We’ll explain what that means a little later.)
var element = document.getElementById('element');

function callback() {
  alert('Hello');
}

// Add listener
element.addEventListener('click', callback);

Demo: addEventListener

Removing Listeners

Removing event listeners once they are no longer needed is a best practice (especially in long-running Web applications). To do this, use theelement.removeEventListener() method:

element.removeEventListener(<event-name>, <callback>, <use-capture>);

But removeEventListener has one catch: You must have a reference to the callback function that was originally bound. Simply callingelement.removeEventListener('click'); will not work.

Essentially, if we have any interest in removing event listeners (which we should in “long-lived” applications), then we need to keep a handle on our callbacks. This means we cannot use anonymous functions.

var element = document.getElementById('element');

function callback() {
  alert('Hello once');
  element.removeEventListener('click', callback);
}

// Add listener
element.addEventListener('click', callback);

Demo: removeEventListener

Maintaining Callback Context

An easy gotcha is callbacks being called with the incorrect context. Let’s explain with an example.

var element = document.getElementById('element');

var user = {
 firstname: 'Wilson',
 greeting: function(){
   alert('My name is ' + this.firstname);
 }
};

// Attach user.greeting as a callback
element.addEventListener('click', user.greeting);

// alert => 'My name is undefined'

Demo: Incorrect callback context

USING ANONYMOUS FUNCTIONS

We expected the callback to correctly alert us with My name is Wilson. In fact, it alerts us with My name is undefined. In order for this.firstName to returnWilsonuser.greeting must be called within the context (i.e. whatever is left of the dot when called) of user.

When we pass the greeting function to the addEventListener method, we are only passing a reference to the function; the context of user is not passed with it. Internally, the callback is called in the context of element, which means thatthis refers to element, not to user. Therefore, this.firstname is undefined.

There are two ways to prevent this context mismatch. First, we can calluser.greeting() with the correct context inside an anonymous function.

element.addEventListener('click', function() {
  user.greeting();
  // alert => 'My name is Wilson'
});

Demo: Anonymous functions

FUNCTION.PROTOTYPE.BIND

The last method isn’t so good because now we don’t have a handle on the function when we want to remove it with .removeEventListener(). Plus, it’s pretty ugly. I prefer to use the .bind() method (built into all functions, as of ECMAScript 5) to generate a new function (bound) that will always run in the given context. We then pass that function as the callback to.addEventListener().

// Overwrite the original function with
// one bound to the context of 'user'
user.greeting = user.greeting.bind(user);

// Attach the bound user.greeting as a callback
button.addEventListener('click', user.greeting);

We also have a reference to the callback at hand, which we can use to unbind the listener if need be.

button.removeEventListener('click', user.greeting);

Demo: Function.prototype.bind

The Event Object

The event object is created when the event first happens; it travels with the event on its journey through the DOM. The function that we assign as a callback to an event listener is passed the event object as its first argument. We can use this object to access a wealth of information about the event that has occurred:

  • type (string)
    This is the name of the event.
  • target (node)
    This is the DOM node where the event originated.
  • currentTarget (node)
    This is the DOM node that the event callback is currently firing on.
  • bubbles (boolean)
    This indicates whether this is a “bubbling” event (which we’ll explain later).
  • preventDefault (function)
    This prevents any default behaviour from occurring that the user agent (i.e. browser) might carry out in relation to the event (for example, preventing a click event on an <a> element from loading a new page).
  • stopPropagation (function)
    This prevents any callbacks from being fired on any nodes further along the event chain, but it does not prevent any additional callbacks of the same event name from being fired on the current node. (We’ll talk about that later.)
  • stopImmediatePropagation (function)
    This prevents any callbacks from being fired on any nodes further along the event chain, including any additional callbacks of the same event name on the current node.
  • cancelable (boolean)
    This indicates whether the default behaviour of this event can be prevented by calling the event.preventDefault method.
  • defaultPrevented (boolean)
    This states whether the preventDefault method has been called on the event object.
  • isTrusted (boolean)
    An event is said to be “trusted” when it originates from the device itself, not synthesized from within JavaScript.
  • eventPhase (number)
    This number represents the phase that the event is currently in: none (0), capture (1), target (2) or bubbling (3). We’ll go over event phasesnext.
  • timestamp (number)
    This is the date on which the event occurred.

Many other properties can be found on the event object, but they are specific to the type of event in question. For example, mouse events will includeclientX and clientY properties on the event object to indicate the location of the pointer in the viewport.

It’s best to use your favorite browser’s debugger or a console.log to look more closely at the event object and its properties.

Event Phases

When a DOM event fires in your app, it doesn’t just fire once where the event originated; it embarks on a journey of three phases. In short, the event flows from the document’s root to the target (i.e. capture phase), then fires on the event target (target phase), then flows back to the document’s root (bubbling phase).

eventflow
(Image source: W3C)

Demo: Slow motion event path

CAPTURE PHASE

The first phase is the capture phase. The event starts its journey at the root of the document, working its way down through each layer of the DOM, firing on each node until it reaches the event target. The job of the capture phase is to build the propagation path, which the event will travel back through in the bubbling phase.

As mentioned, you can listen to events in the capture phase by setting the third argument of addEventListener to true. I have not found many use cases for capture phase listeners, but you could potentially prevent any clicks from firing in a certain element if the event is handled in the capture phase.

var form = document.querySelector('form');

form.addEventListener('click', function(event) {
  event.stopPropagation();
}, true); // Note: 'true'

If you’re unsure, listen for events in the bubbling phase by setting theuseCapture flag to false or undefined.


LATEST POSTS