This is a tutorial text on “events bubbling” in JavaScript. Some theory is explained, and several JavaScript examples are shown.
1 Introduction
The purpose of this article is to provide an illustrative example of the “bubbling and capturing of events” in browser DOM. This is not meant to be a complete tutorial, but rather a “proof of concept” of how events propagate in DOM.
2 Theoretical Background
Every DOM Node can generate an event. An event is a signal that something happened.
To react to an event, we need to assign a Node an event handler. There are three ways to assign a handler:
- Html attribute usage. For example:
onclick=”myhandler(e)”;
- DOM property usage. For example:
element.onclick=myhandler;
- JavaScript method. For example:
element.addEventListener(e, myhandler, phase);
The existence of the method addEventListener
is coming from the fact that every Node
in DOM inherits from EventTarget
[2] which acts as a root abstract class. So, every Node
can receive events and react on them via an event handler.
Most events in DOM propagate. Typically, there are three phases of event propagation:
- Capturing phase: Propagation from window object towards specific target event.
- Target phase: The event has reached its target.
- Bubbling phase: Propagation from the target towards the window object.
An event object is being thrown when an event happens. It contains properties describing that event. Two properties are very interesting to us:
event.currentTarget
– object that handled the event (for example, some parent of an element that was actually clicked, that got that event by the act of bubbling) event.target
– target element that initiated the event (for example, the element on which it was actually clicked)
3 Example01 - Simple Event Propagation Demo
3.1 Going Over All Nodes
First, we want to create a list of all Event recipients of interest. That will include all Nodes in our DOM plus document
and window
objects. Here is the code for that:
function getDescendants(node, myArray = null) {
var i;
myArray = myArray || [];
for (i = 0; i < node.childNodes.length; i++) {
myArray.push(node.childNodes[i])
getDescendants(node.childNodes[i], myArray);
}
return myArray;
}
function CreateListOfEventRecipients() {
let result;
result = getDescendants(document);
result.push(window.document);
result.push(window);
return result;
}
3.2 Logging Events
Next, we want to nicely log events in our event-handler function. Here is the code for that:
function EventDescription(e, handlerPhase) {
const dots = "......";
let result;
if (e !== undefined && e !== null) {
let eventObject = e.toString();
let eventType = (e.type) ?
(e.type.toString()) : undefined;
let eventTimestamp = (e.timeStamp) ?
(e.timeStamp.toString()) : undefined;
let eventTarget = (e.target) ? ObjectDescription(e.target) : undefined;
let eventCurrentTarget = (e.currentTarget) ?
ObjectDescription(e.currentTarget) : undefined;
let eventPhase = (e.eventPhase) ?
PhaseDescription(e.eventPhase) : undefined;
result = "";
result += (eventTimestamp) ? eventTimestamp : "";
result += (eventObject) ? eventObject : "";
result += "-----------<br>";
result += dots;
result += (eventType) ? ("EventType:" + eventType) : "";
result += (handlerPhase) ? ("..HandlerPhase:" + handlerPhase) : "";
result += (eventPhase) ? ("..EventPhase:" + eventPhase) : "";
result += "<br>";
result += (eventTarget) ? (dots + "Target:" + eventTarget + "<br>") : "";
result += (eventCurrentTarget) ? (dots + "CurrentTarget:" +
eventCurrentTarget + "<br>") : "";
}
return result;
}
3.3 Full Example01 Code
Here is the full Example01
code, since most people like code that they can copy-paste.
<!DOCTYPE html>
<html>
<body>
<!--
<div id="Div1" style="border: 1px solid; padding:10px;
margin:20px; background-color:aqua;">
Div1
<div id="Div2" style="border: 1px solid; padding:10px;
margin:20px;background-color:chartreuse">
Div2
<div id="Div3" style="border: 1px solid; padding:10px;
margin:20px; background-color:yellow; ">
Div3<br/>
Please click on
<b id="Bold4" style="font-size:x-large;">bold text</b> only.
</div>
</div>
</div>
<hr />
<h3>Example 01</h3>
<br />
<button onclick="task1()"> Task1-List of all nodes</button><br />
<br />
<button onclick="task2()"> Task2-Activate EventHandlers</button><br />
<br />
<button onclick="task3()"> Task3-Clear Output Box (delayed 1 sec)</button><br />
<hr />
<h3>Output</h3>
<div id="OutputBox" style="border: 1px solid; min-height:20px">
</div>
<!--
<script>
function OutputBoxClear() {
document.getElementById("OutputBox").innerHTML = "";
}
function OutputBoxWriteLine(textLine) {
document.getElementById("OutputBox").innerHTML
+= textLine + "<br/>";
}
function OutputBoxWrite(textLine) {
document.getElementById("OutputBox").innerHTML
+= textLine;
}
function ObjectDescription(oo) {
let result;
if (oo != null) {
result = "";
result += oo.toString();
if (oo.nodeName !== undefined) {
result += ", nodeName:" + oo.nodeName;
}
if (oo.id !== undefined && oo.id !== null
&& oo.id.trim().length !== 0) {
result += ", id:" + oo.id;
}
if (oo.data !== undefined) {
let myData = oo.data;
let length = myData.length;
if (length > 30) {
myData = myData.substring(0, 30);
}
result += `, data(length ${length}):` + myData;
}
}
return result;
}
function PhaseDescription(phase) {
let result;
if (phase !== undefined && phase !== null) {
switch (phase) {
case 1:
result = "1 (Capturing)";
break;
case 2:
result = "2 (Target)";
break;
case 3:
result = "3 (Bubbling)";
break;
default:
result = phase;
break;
}
}
return result;
}
function EventDescription(e, handlerPhase) {
const dots = "......";
let result;
if (e !== undefined && e !== null) {
let eventObject = e.toString();
let eventType = (e.type) ?
(e.type.toString()) : undefined;
let eventTimestamp = (e.timeStamp) ?
(e.timeStamp.toString()) : undefined;
let eventTarget = (e.target) ? ObjectDescription(e.target) : undefined;
let eventCurrentTarget = (e.currentTarget) ?
ObjectDescription(e.currentTarget) : undefined;
let eventPhase = (e.eventPhase) ?
PhaseDescription(e.eventPhase) : undefined;
result = "";
result += (eventTimestamp) ? eventTimestamp : "";
result += (eventObject) ? eventObject : "";
result += "-----------<br>";
result += dots;
result += (eventType) ? ("EventType:" + eventType) : "";
result += (handlerPhase) ? ("..HandlerPhase:" + handlerPhase) : "";
result += (eventPhase) ? ("..EventPhase:" + eventPhase) : "";
result += "<br>";
result += (eventTarget) ? (dots + "Target:" + eventTarget + "<br>") : "";
result += (eventCurrentTarget) ? (dots + "CurrentTarget:" +
eventCurrentTarget + "<br>") : "";
}
return result;
}
function getDescendants(node, myArray = null) {
var i;
myArray = myArray || [];
for (i = 0; i < node.childNodes.length; i++) {
myArray.push(node.childNodes[i])
getDescendants(node.childNodes[i], myArray);
}
return myArray;
}
function CreateListOfEventRecipients() {
let result;
result = getDescendants(document);
result.push(window.document);
result.push(window);
return result;
}
function MyEventHandler(event, handlerPhase) {
OutputBoxWrite(EventDescription(event, handlerPhase));
}
function BubblingEventHandler(event) {
MyEventHandler(event, "Bubbling");
}
function CapturingEventHandler(event) {
MyEventHandler(event, "Capturing");
}
window.onerror = function (message, url, line, col, error) {
OutputBoxWriteLine(`Error:${message}\n At ${line}:${col} of ${url}`);
};
function task1() {
setTimeout(task1_worker, 1000);
}
function task1_worker() {
OutputBoxClear();
OutputBoxWriteLine("Task1");
let arrayOfEventRecipientCandidates = CreateListOfEventRecipients();
for (let i = 0; i < arrayOfEventRecipientCandidates.length; ++i) {
let description = ObjectDescription(arrayOfEventRecipientCandidates[i]);
OutputBoxWriteLine(`[${i}] ${description}`);
}
}
function task2() {
OutputBoxClear();
OutputBoxWriteLine("Task2");
setTimeout(task2_worker, 1000);
}
function task2_worker() {
let arrayOfEventRecipientCandidates = CreateListOfEventRecipients();
for (let i = 0; i < arrayOfEventRecipientCandidates.length; ++i) {
if ("addEventListener" in arrayOfEventRecipientCandidates[i]) {
arrayOfEventRecipientCandidates[i].addEventListener
("click", BubblingEventHandler);
arrayOfEventRecipientCandidates[i].addEventListener
("click", CapturingEventHandler, true);
}
else {
let description = "Object does not have addEventListener:" +
ObjectDescription(arrayOfEventRecipientCandidates[i]);
OutputBoxWriteLine(`[${i}] ${description}`);
}
}
}
function task3() {
setTimeout(OutputBoxClear, 1000);
}
</script>
</body>
<!--
</html>
3.4 Application Screenshot
Here is what the application looks like:
3.5 Execution – Finding all Event-Targets
First, we will show all Event Targets that our methods find. That includes all Nodes plus document
and window
objects. There are around 60 objects on that list. Note that it includes ALL Nodes, including text and comment nodes.
3.6 Execution – Activating Event-Handlers
Then we activate Event-Handlers for all objects in the list.
3.7 Execution – Click Event
Now we do our click. Here is a log of events from the application:
3.8 Comments
For those who like DOM tree diagrams, here is a diagram of what happened in the application.
- Please note that the above diagram does not include
document
and window
objects that, as can be seen from the log, are also recipients of the click event. - Please note that although we actually clicked text Node #text-6, that Node did not receive the event, but the Element that contains it B Id:Bold4 did receive the click event.
4 Example02
The reason I created this example is because I saw claims on the Internet that in the Target phase, events are not always run in Capturing-Bubbling
order, but in order that they are defined in the code. I find those claims untrue, at least for this version of Chrome I used for testing.
4.2 Code
I will not put the entire code here, since it is similar to the previous example. Here are just key parts:
function task1() {
OutputBoxClear();
OutputBoxWriteLine("Task1-Activate EventHandlers");
let div1=document.getElementById("Div1");
div1.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling"));
div1.addEventListener("click", (e)=>MyEventHandler(e, "Capturing"), true);
let div2=document.getElementById("Div2");
div2.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling"));
div2.addEventListener("click", (e)=>MyEventHandler(e, "Capturing"), true);
let div3=document.getElementById("Div3");
div3.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling"));
div3.addEventListener("click", (e)=>MyEventHandler(e, "Capturing"), true);
div3.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling2"));
div3.addEventListener("click", (e)=>MyEventHandler(e, "Capturing2"), true);
}
Please note that in (**), we interleaved definition of Capturing
and Bubbling
event handlers for the same element.
4.3 Screenshot
Here is the application screenshot.
4.4 Execution – Our Click
Now we do our click. Here is a log that is produced:
As far as I see (on this version of Chrome), in the Target phase, all events are run in order of Event-Handlers Capturing first, then Bubbling
. Handlers are not run in order as defined in (**).
If you look at the Target Phase (yellow), you will see the order of events “Capturing”, “Capturing2”, “Bubbling”, “Bubbling2” in this execution. What some people on the internet claimed is that the order will be the same as in definition (**), something like “Capturing”, “Bubbling”, “Capturing2”, “Bubbling2”. But, this experiment of mine disproves that claim, at least for this version of Chrome.
5 Conclusion
In this article, we provided a simple “proof-of-concept” application that shows how event propagation in DOM works.
6 References
7 History
- 13th November, 2023: Initial version