Currently doing a JavaScript Series on some interesting Javascript topics if you have missed the previous ones check them out here. If you have been here before ๐
Learning JavaScript? Topic: Hositing -- This was recently updated
Learning JavaScript? Topic: Higher Order Functions -- Currently the ๐ฅ
Now lets begin on todays Topic - Event Bubbling and Delegation
Introduction to the DOM
When a web page is loaded, the browser creates a Document Object Model of the page. The HTML DOM model is constructed as a tree of object nodes
The nodes in the node tree have a hierarchical Relationship to each other. The terms parent, child and sibling are used to describe the relationship. At the very top of the Node tree is the root node, and every node has one parent apart from the root node. A node can have several children and a node with
The same parent are known as siblings.
This DOM node relationship is the focus of this article when dealing with Event Bubbling and Delegation.
Event Bubbling
Event Bubbling is the propagation of events through the DOM (Document Object Model), meaning when an event happens on a particular element in the DOM it would lift up
to its parents and call the events if they have any attached to them.
Example of Event Bubbling
Example - Here is a HTML file that is used to render a Task Input form. Once the user types out a task,
they can click on add to insert it to the HTML div tag with class .collection-box.
PS . This form container uses bootstrap for styling
<div class="container">
<div class="row">
<div class="col s12">
<div id="main" class="card">
<div class="card-content">
<span class="card-title">Task List</span>
<div class="row">
<form id="task-form">
<div class="input-field col s12">
<input type="text" name="task" id="task" autocomplete="off">
<label for="task">New Task</label>
</div>
<input type="submit" value="Add Task" class="btn">
</form>
</div>
</div>
<div class="card-action">
<h5 id="task-title">Tasks</h5>
<ul class="collection">
<div class="collection-box">
</div>
</ul>
<a href="#" class="clear-tasks btn black">Clear Tasks</a>
</div>
</div>
</div>
</div>
</div>
Assuming in our script file we have event listeners on the parent elements of the li tag we would be inserting into .collection-box the code will look something like this
const form = document.querySelector('#task-form');
const taskList = document.querySelector('.collection-box');
const collectionBox = document.querySelector('.collection-box');
// Load all event listeners
loadEventListeners();
// Load all event listeners
function loadEventListeners() {
// Add task event
form.addEventListener('submit', addTask);
taskList.addEventListener('click', function(){
console.log("Parent is also being called")
})
collectionBox.addEventListener('click', function(){
console.log("Child was clicked")
})
}
// Add Task
function addTask(e) {
//implementation of addTask goes here
e.preventDefault();
}
But because of Event Bubble what happens is that when the user clicks on the x button to delete the element from the task list it tends to fire up the event listeners for the parent elements as well. Which we dont want, instead we want a targetted event to occur. See below the console log of what happens when we try to delete the HTML li tag.
How can we fix this? Event Delegation to the rescue ๐. But before we look at Event Delegation in practice, lets talk about the scenarios in which event delegation should be used.
Scenario 1
When there is a list of items in a table or an ordered/unorder list and we need to perform an action on each individual row like delete, or update.
Scenario 2
When we are dynamically adding items to a list with other items. Then event Delegation comes in handy.
Event Delegation
Event Delegation is when we put an event listener on a parent element so we can target the child element we want the event to occur on.
Implementing Event Delegation
Since event bubbling triggers events of the child's parent, to prevent this behaiviour, we want to target the child element that we would like the event to happen. So therefore, we would set our event listener on the parent element and target a child element (delegate to a child element) where the event should happen. First off if we look at the implementation of addTask we can see that the element we should be
targeting is the link tag with class name .delete-item
// Add Task
function addTask(e) {
if(taskInput.value === '') {
alert('Add a task');
}
// Create li element
const li = document.createElement('li');
// Add class
li.className = 'collection-item';
// Create text node and append to li
li.appendChild(document.createTextNode(taskInput.value));
// Create new link element
const link = document.createElement('a');
// Add class
link.className = 'delete-item secondary-content';
// Add icon html
link.innerHTML = '<i class="fa fa-remove"></i>';
// Append the link to li
li.appendChild(link);
// Append li to ul
taskList.appendChild(li);
// Clear input
taskInput.value = '';
e.preventDefault();
}
But first lets try and only target the event firing when we click the x button to delete the item. We do this like below
const taskList = document.querySelector('.collection-box');
// Load all event listeners
loadEventListeners();
function loadEventListeners() {
// Add task event
form.addEventListener('submit', addTask);
//Only add event listener to the parent element of target child
taskList.addEventListener('click', deleteItem)
}
//delete task input
function deleteItem(e){
if(e.target.parentElement.className === "delete-item secondary-content"){
console.log("This li is to be deleted")
}
}
Now instead of adding event listeners everywhere we have simply delegated the action to the particular child element we want to perform an action.
Notice that the targetted element has to call its parentElement class name, this is because the x icon is wrapped within a link tag. Selecting the item this way ensures that the icon is targeted properly
It is also advisable not to compare the className using the above instead its better to check that the className exsist using the below just incase another className is dynamically added which would make our comparison above invalid
//Instead do this for delete task input
function deleteItem(e){
if(e.target.parentElement.classList.contains("delete-item")){
//This selects the link tag, then the li tag to remove the list item
e.target.parentElement.parentElement.remove();
}
}