Dylan Barnard    About    Archive    Projects

6 Questions Towards a Better Understanding of the JS Event Object

A clear crystal 1 Asking questions to get Crystal Clear™

The ability to execute code in a browser environment has made JavaScript one of the most important programming languages of the day. One interesting thing about application code on the browser is that it usually needs to wait for something to transpire first - such as a user’s click. An event object represents information related to how, what, and when this occurrence happened.

When I ran into the concept of events I wanted to be sure I was pretty clear on the principles that underly JavaScript’s ability to create, register, listen, and execute event-based code.

Below are some questions, and hopefully answers, that mirror some of the confusions I had about JavaScript events.

Events - If only we listen, they will be heard

The browser and DOM are firing events all the time regardless of what code we’ve written.

Besides obvious ones like a user clicking on the page, we also have things like the DOMContentLoaded event, which occurs when the DOM is parsed from HTML, and Window.onload, fires once all of the page’s assets are finished loading. Besides certain browser based events affecting how quickly how our application runs, they can’t influence what code our application runs until we listen for them.

Event-based code is the process of tying the execution of these events to code in our applications.

The Click Event: There and Back Again

So, let’s briefly overview what event implementation, capturing, bubbling are to make sure we’re on the same page.

Setting up an event listener

Elements, windows, documents and more all implement the EventTarget interface which allows developers to add EventListeners (usually) via the addEventListener method which specifies an event type and a callback.

document.addEventListener('click',
  function() { alert('Clicked!' );
});

But you can also use global event handlers either in your JS or HTML if you apply it as a node attribute.

<button onclick="console.log('clicked')">
  Click me
</button>

Event DOM traversal & Propogation

An event traversing through the DOM tree

There are many overviews of DOM event traversal, but this one is mine

When an element dispatches an event, all parent elements have the event fire in a capturing phase, the target fires in the target phase, and parent elements again fire in the bubbling phase. The listener by default only executes the event handler code in the target and bubbling stages.

This may seem a bit theoretical at this point, but I’ll be referencing this later on.

1) How exactly does the target property get set?

Here is the definition of the target property from the MDN documentation.

A reference to the object that dispatched the event

Ok, seems straightforward. You click on something and that’s the element which dispatches the event. My follow up question was - if I add an event listener to a parent element, will the inner element or the parent element dispatch the event?

A visual representation usually helps me, so I made the simplest one I could. In this example, I attached an event listener to the div.button-container element, outlined in red.

document.addEventListener('DOMContentLoaded', function() {
  document.querySelector('.button-container').addEventListener('click', logger);
});

The event listener is listening for the built-in click event, so try clicking on both the button and div to see what happens.

See the Pen Bound container - target by Dylan (@dylankb) on CodePen.

You’ll notice that the target element changes depending on which of the two elements you click, regardless of whether it is nested or not.

Referencing a description for the click event’s target property helped me think about how the target property is actually determined.

The event target (the topmost target in the DOM tree).

A small side quibble. Most representations of the DOM are “upside-down”, in that nested elements are the branches that extend downwards. Here’s a visual of the current markup in this example.

DOM tree node visual using button and container from post 2

Note that the button element is “lower” than the div. Based on how this tree is represented, I’m interpreting “topmost target” to actually mean “bottom-most”, as that would make sense - if we click the button element, it’s the farthest down and is the one that is being set to target. “Top” or “above” usually means towards the “root”. Anyways, quibble out.

Another way of describing how the target property is determined is on this page comparing different event targets.

The DOM element on the left-hand side of the call that triggered this event, eg: element.dispatchEvent(event)`

There may be some further implementation details that could help us determine which element calls the dispatchEvent, but that may be a bit too low level for the purposes of application development.

TLDR; In the example of the click event, the element you click on should reliably be the target property.

2) How does a child element dispatch an event to a parent listener?

Remember when I said that funny looking chart with arrows would be important, well that time has come.

Here, I’ll even show it again for good reference.

An event traversing through the DOM tree

So recall that all events:

  • Travel up and down their DOM tree branch twice.
  • The listener by default only executes the event handler code in the target and bubbling stages.

Now that we understand how traversal works and how the target property is set, we have the tools to understand how an event on a child element triggers a parent listener. If it isn’t clear yet, bubbling is a key player here.

Dispatched events will bubble up from child elements, and by default will trigger parent listeners. Therefore, a click on button will fire the event handler attached to div.button-container.

3) How do the target and currentTarget properties vary under different settings?

First, let’s look into how the currentTarget is determined. Here’s the definition from the docs:

… It always refers to the element to which the event handler has been attached, as opposed to event.target which identifies the element on which the event occurred.

Recall that our event listener is attached to the div. Given this fact and the above definition, we can probably guess what the current target will be, but a coded visual always helps:

See the Pen Bound container - targets by Dylan (@dylankb) on CodePen.

Whether we click on the button or the div, the currentTarget is always the div. That makes sense, as the div is “the element to which the event handler” has been attached.

Going back to the definition of currentTarget, here’s a preceding sentence that I left out earlier:

Identifies the current target for the event, as the event traverses the DOM.

Ah, interesting. More information begs more questions…

4) What are the implications of event DOM traversal?

Ok, so with that in mind, let’s create a second additional separate event handler function called innerLogger. We will attach this listener to the nested button element.

document.querySelector('.button').addEventListener('click', innerLogger);

The functionality is entirely the same - all that’s different is it prepends “INNER” to all our previous output. Try clicking the two page elements in the results tab.

See the Pen Bound container - bubbling by Dylan (@dylankb) on CodePen.

Hmm, when clicking on button our output appears to be the same as before - it’s as if nothing seems to have changed. What’s happened is we’ve created a subtle bug. If you were to insert a debugging statement or a console.log within innerLogger, you would see that the innerLogger event handler does indeed fire. The problem is that our logger event handler ALSO fires. In the first question we already confirmed that parent elements with event listeners will fire when the specified event occurs on their child elements. In this case, a button click bubbles event travels up through each successive parent element until reaching the window element. The effect is that the output of innerLogger is replaced, or overwritten, by the output of logger.

In this case, stopping the continued propagation of the click event is the solution. Comment out line 2 in the Codepen to see a working solution.

In summary, one implication of DOM traversal is parent elements that share the same click event may fire. The reason that parent elements may fire after is due to bubbling. If we were using capturing, the result would be the parent elements or “top” elements would fire first.

5) Is this equal to currentTarget or target inside event handler callback functions?

The answer is by default - sometimes both, but almost always this is the same as currentTarget.

Let’s talk about why this is. This page contains the first explanation on how the this binding is set.

When the event handler is invoked, the this keyword inside the handler is set to the DOM element on which the handler is registered.

The explanation is fairly understandable; this will be set to the element with the attached event listener. When using default bubbling, sometimes the element with the attached listener also dispatches the event to become the target (i.e clicking on the container div), and sometimes a nested element dispatches the event (i.e the button). That’s why the answer is “sometimes both”.

Let’s log out the this context to confirm our understanding.

See the Pen Bound container - this by Dylan (@dylankb) on CodePen.

Confirmed. In this case, the event listener was attached to the div. Therefore within the callback, div is both the currentTarget and the this context.

With all that said, the provided explanation may be slightly superficial, but we’ll revisit why in just a bit.

6) Does event delegation affect event object properties and context differently?

Event delegation lets you avoid adding individual event listeners to each element, and instead control which child events fire through attaching an event listener to a parent and adding additional code.

We could change our code to take advantage of event delegation through making changes like:

  • Attaching our event listener to the document, and avoid the DOMContentLoaded event to fire
  • Add additional nested elements inside the button-container and have them not fire event listeners, behave differently, or whatever we want.

We’re not going to make those changes right now. Instead, we’ll do a somewhat contrived, but very simple, implementation of event delegation. We’ll wrap our logger code in a small conditional.

function logger() {
  if (e.target.tagName === "BUTTON") {
    // Leave existing logger code
}

The main body of our logger code will now only fire when the button is clicked, and not when the outer button-container is clicked.

See the Pen Bound container - delegation by Dylan (@dylankb) on CodePen.

As you can see, the other event properties are exactly the same as before event delegation. The currentTarget is still the div, the target remains button when clicking on the button, etc.

The story is a bit different with jQuery, however.

Understanding jQuery delegation principles

First, I want to go back to an issue I said I had about an excerpt from the this binding documentation. The excerpt says this is “… set to the DOM element on which the handler is registered”, which works fine for plain JavaScript. However, I prefer this definition from a different part of the MDN docs covering this.

When a function is used as an event handler, its this is set to the element the event fired from

The reason why is that in jQuery the on method allows for an optional selector parameter.

$('.button-container').on('click', '.button', logger);

Using jQuery parlance, up to now we’ve been directly binding elements (event targets) to their event listeners. Delegated events in jQuery have slightly different rules, as explained here

The handler is not called when the event occurs directly on the bound element, but only for descendants (inner elements) that match the selector.

To see the implications, click on the button below:

See the Pen Delegated container - jQuery by Dylan (@dylankb) on CodePen.

Events are still attached to div, however, the currentTarget and this context are now both button. To rephrase the jQuery documentation above, this is because if a selector is provided, the event occurs not on the element the event listener was added to, but on the element(s) matching the selector within the parent. With our additional selector of .button, elements matching the selector are now the ones that call event listeners, and NOT the parent with the attached event.

Further reading:

  1. MDN - Introduction to Events
  2. MDN - Overview of Events and Handlers
  1. Photo / CC BY 

  2. Screenshot taken using https://dom-viewer.herokuapp.com/