In previous articles, we looked at various event registration methods. Out of those, the most commonly used method is addEventListener/removeEventListener due to its ability to accept configuration parameters.
In case you have missed previous articles , here are 3 different forms of addEventListener.
addEventListener(type, callback)
addEventListener(type, callback, useCapture)
addEventListener(type, callback, { capture: true, once: false, passive: false })
We know useCapture and capture are used to register an event for different phases.
The chicken and egg problem of the event handlers
It is obvious to infer from all event registration syntaxes that there is only 1 element involved in the entire event handling mechanism. And it is the one we used to register an event with.
I assumed that too until I came across an interesting situation.
Consider an HTML table as shown in the image. There are 2 requirements for this table from an interaction point of view.
- A user should be able to check/uncheck the box.
- A user should be able to click an entire row and navigate to the details page.
Hmm.. They don't seem tricky at first but give it a little more thought and you will see the problem.
That's right. There is a conflict of requirements from the developer's eye. A checkbox is also part of the entire row. So when the user clicks on a checkbox, what do we do? Do we fire the handler for the checkbox click event? Or do we fire the handler for the row click event?
I have reproduced this situation in a codepen. You can see it by clicking on this link.
if you notice, when we toggle the checkbox, row click handler is also invoked. But while clicking on a row, the checkbox handler is not invoked.
This happens due to an interesting phenomenon called as EVENT FLOW.
What is an EVENT FLOW
It is the browser's duty to notify the element about the event occurrence. But the browser does something more than that.
It traverses from the root element to the element on which the event occurred, and from this element, it traverses back to the root element. It also notifies every element along the way about the event.
This single round trip that happens per event is called an EVENT FLOW.
Event Phases
An event flow is divided into 3 phases.
- Capture phase
- Target phase
- Bubble phase
Capture phase
This is the phase where the browser travels from the root ancestor to the parent of the element where the event actually occurred. During this phase, the browser notifies every element along the way. It also checks if those elements have an event listener set up for the CAPTURE phase. If it finds one it executes it.
Target phase
This is the phase where the browser reaches the element where the event actually occurred. In this phase, the browser executes the event listener attached to such an element.
Bubble phase
This is the phase where the browser starts the return journey from the element where the event occurred to the root ancestor. While going back, it again notifies every element along the way. It checks if any event listeners are set up for the BUBBLE phase. If it finds one it executes it.
Set up an event listener for a PHASE
In an event flow, browser visits all the element in the path twice (except for the element on which the event occur). At both times, it tries to find an event listener for particular phase and executes it.
How does the browser know that an event is registered for a particular phase?
Let's take a look at an example to answer this question
<div id="outer">
<div id="inner">
<button id="button">Click</button>
</div>
</div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const button = document.getElementById('button');
With this basic example, we will try to do the following things
- Set click listener on outer in the capture phase
- Set click listener on inner in the bubble phase
- Set click listener on the button in target phase (This is very interesting)
1) Set click listener on outer in the capture phase
There are two ways of doing it. But the last one is easy to understand
Passing useCapture(3rd parameter) as true
outer.addEventListener('click', function(){/*code*/}, true);
Setting capture property of options object to true
outer.addEventListener('click', function(){/*code*/},{ capture: true });
2) Set click listener on inner in the bubble phase
In order to set any event listener in the bubble phase, we need to assign false to either useCapture or capture from the options object. But both of these are set to false by default.
This means every event listener we create using addEventListener is set up for the bubble phase by default.
Here is how it will look.
inner.addEventListener('click', function(){/*code*/});
3) Set click listener on button in the target phase
There is no option to explicitly set an event listener in the target phase. A click listener which is associated with the element upon which the event originally occurred, is said to be in the target phase.
e.g. If click event originates on the button element, then click listener of button is said to be in the target phase. Similarly, if it originates from inner, then click listener of inner is said to be in target phase.
(In my upcoming articles, I will show you how to check the phase of an event listener.)
Now that we know which event listener is set up for which phase, let's do a dry run for our example.
Let us analyze the sequence of actions assuming the user clicks on the button
- Event flow starts in the capture phase at the root of the document and reaches outer.
- It finds a click event listener on outer set up for capture phase and executes it.
- It reaches inner but does not find any click event listener for the capture phase. The capture phase ends here.
- Event flow now reaches button. This is the target phase. There is a click event listener which is executed. The target phase ends here and event flow now starts the return journey in the bubble phase.
- Event flow reaches inner once again but in the bubble phase. It finds that there is a click listener set up for the bubble phase. It executes it.
- Event flow now reaches outer once again but there is no click event listener set up for the bubble phase.
- Event flow completes the journey back to the root ancestor.
I know it is annoying to read CLICK listener every damn time. But I have done it intentionally. It is due to the fact that a round trip of an event flow is triggered by a specific type of event such as click, focus, blur, etc. Hence, it finds and executes the same type of events in the capture/bubble/target phase on all events on its way.
Suppose, in our example, we add a blur event on outer in the bubble phase, it won't be executed by the event flow of a click event in either of its phases.
References
Finally
I do remember that we started this post with a practical example of a table, a row, and a checkbox. By now you should be able to justify why two click handlers are invoked every time we toggle the checkbox.
In my next article, we will see how to get around this issue as well as some patterns of event handling that are recommended for performance enhancements and cleaner code.
Stay tuned...