All Articles

A modern guide to Events in JavaScript

Published 19 Aug 2019 · 15min coffee icon22 min read

What are Events?

Events are objects and implement the Event interface

Those objects are dispatched by the user agent (or the application itself) and handled by the Event Listener. Events are responsible for handling user interactions or network activity. There are different types of events but you can check then on MDN if you want. We have to focus on how events are propagating through the DOM.

Event propagation

First, we have to create some simple DOM to explain event propagation

<div id="container">
  <div id="dataContainer">
    <div id="clickedData"></div>
  </div>
  <button id="dataSubmitter">Add Data</button>
</div>

Object

To handle events we need to add some Event Listeners. To do that in VanillaJS we have to get DOM element and call addEventListener on it. DOM Element is implementing EventTarget interface and because of that, we have access to addEventListener(type, callback, options) and removeEventListener(type, callback, options) methods.

  • type - the name of the event
  • callback - function to be called
  • options - object to change default listener behavior
const button = document.getElementById('dataSubmitter');

button.addEventListener('click', e => {
  console.log('clicked');
});

After calling addEventListener we’ve added a listener to event listener list on that element (button). This list is a list of zero or more event listeners which means you can add more listeners for the same event.

const button = document.getElementById('dataSubmitter');

button.addEventListener('click', e => {
  console.log('clicked first');
});
button.addEventListener('click', e => {
  console.log('clicked second');
});

In this case when click event is dispatched, listeners will be called in the order in which they were assigned (we’ll see clicked first and then clicked second in the console).

Everyone knows what will happen after clicking on our button (2 console logs) but to show how the event is propagating through our DOM tree we have to add more listeners

Object

And now after user interaction event is bubbled through the DOM upwards

Object

This process is called bubbling and it’s easy to understand, but it’s not the whole path. Every event starts its path from the top of the tree. This process is called capturing and to access events in the capture phase we have to create listeners with extra options passed as the third argument.

const container = document.getElementById('container');
container.addEventListener(
  'click',
  e => {
    console.log("container capture 'click'");
  },
  {
    capture: true,
  }
);

capture: true informs our listener to listen for events in phase 1 or 2. Every event has 3 phases:

  • Capture (1) - going down the tree
  • On Target (2) - that’s self-explanatory
  • Bubble (3) - going up the tree

Now let’s see how our event is propagating (numbers next to event type is the order in which event listener is added).

Object

After click event is dispatched it starts at the body level with eventPhase === 1. After that is propagates through the tree until it reaches its target (button).

At the button level phase change to 2 and we can spot unusual behavior. If you look closely on the order in which listeners are called (button), first we’re calling event listeners which are responsible for bubbling event and then listener responsible for capturing. It might seem weird at the beginning, but at the button level, our event is not in any of those phases. Because of that, our listeners are executed in order of creation. In On Target phase, listener type doesn’t matter.

After that our event is bubbling in the same way as in the previous example.

How to stop events from propagating?

Sometimes we want our events to top at the specific point of the tree. To do that we need to use e.stopPropagation() or e.stopImmediatePropagation(). Those functions are preventing other listeners from being called with the same event.

stopPropagation()

const button = document.getElementById('dataSubmitter');

button.addEventListener('click', e => {
  console.log('clicked first');
  e.stopPropagation();
});
button.addEventListener('click', e => {
  console.log('clicked second');
});
button.addEventListener(
  'click',
  e => {
    console.log("button capture 'click'");
  },
  {
    capture: true,
  }
);

Object

Even if we call stopPropagation() inside the first listener, all listeners assigned to current elements are called. Event is not propagated into another EventTarget. We can call stopPropagation() inside any of our listeners on the way, it doesn’t have to be target or even bubbling phase. If you want to prevent the event from reaching the target you can call stopPropagation in one of the listeners listening on the capture phase.

stopImmediatePropagation()

const button = document.getElementById('dataSubmitter');

button.addEventListener('click', e => {
  console.log('clicked first');
  e.stopImmediatePropagation();
});
button.addEventListener('click', e => {
  console.log('clicked second');
});
button.addEventListener(
  'click',
  e => {
    console.log("button capture 'click'");
  },
  {
    capture: true,
  }
);

Object

You’ve probably already spotted the difference. stopImmediatePropagation is stopping propagation on the same listener in which it’s called. If we call it inside click (2) then propagation stops at click (2) and doesn’t execute click (3). You should be aware of that difference when dealing with multiple listeners on your elements.

preventDefault()

It’s a good time to mention preventDefault method. It’s quite common in modern frameworks to do something like that:

e.preventDefault();
e.stopPropagation();

A lot of people are doing that without even knowing why. You know that stopPropagation() is stopping an event from going through the event listeners. preventDefault on the other hand, is telling the browser not to execute its default behavior.

If we call preventDefault() on click event from checkbox then browser won’t change our checkbox to the selected state. We can handle our event manually (e.g. apply custom action instead). That’s also helpful when it comes to submitting the form. In most of the popular libraries, we don’t actually want form element to send HTML from. We want to handle form data manually, apply validation, parsing and add some extra data before sending.

Dispatching events manually

I’ve said before, event doesn’t have to be created by the browser, we can create them. To do that we have to create new CustomEvent instance, and dispatchEvent. CustomEvent is basically the same standard Event but has an additional attribute in options object called details. That attribute is used to store custom data.

const clickedData = document.getElementById('clickedData');
const dataEvent = new CustomEvent('addData', {
  detail: { test: 'test2' },
});

clickedData.dispatchEvent(dataEvent);

Let’s add listeners to our tree to listen for our custom event

Object

What will happen when our code is executed?

Object

Wait for the second, why there is no bubbling?! In our case event stays on On Target phase. Reason for that is because by default all events created using new CustomEvent() have bubbles set to false.

To enable bubbling for this event we have to create it with options object:

new CustomEvent('addData', {
  bubbles: true,
  detail: { test: 'test2' },
});

Object

Using CustomEvent is not mandatory. If you want you can create instance of new Event() and use it for your propagation.

When we might want to use custom events?

I think sending data to elements it’s obvious. A good example to show you why you might want to hook up into event propagation is React’s HOCs (Higher Order Components). Imagine an application with multiple layers of logic wrapped around your form. It might look sth like:

<div id="app">
  <div id="addUserData">
    <div id="prefillAfterReset">
      <form>
        {...}
      </form>
    </div>
  </div>
</div>

In this case, we can hook into form’s submit and reset events even before they reach our form component and add/modify data on submitting/resetting the form. I’m not saying to implement such behavior because there are better ways to do it.

If you want to play with events please check this sandbox

Citation

Kemal Erdem, (Aug 2019). "A modern guide to Events in JavaScript". https://erdem.pl/2019/08/a-modern-guide-to-events-in-java-script
or
@article{erdem2019aModernGuideToEventsInJavaScript,
    title   = "A modern guide to Events in JavaScript",
    author  = "Kemal Erdem",
    journal = "https://erdem.pl",
    year    = "2019",
    month   = "Aug",
    url     = "https://erdem.pl/2019/08/a-modern-guide-to-events-in-java-script"
}