Event Delegation in JS

Event Delegation in JS

Introduction

Event delegation is a technique for handling events in javascript. In this technique instead of applying event listeners to the individual target elements, listeners are attached to their common parent element.

We don't always use this technique, this technique is useful in cases wherein we want to use the same event listener for multiple elements to achieve the same functionality.

But why attach on the common parent if we can attach that to individual children elements?

Let's try to understand this with the help of an example.

Say we are building a todo app which will have the following features.

  1. show all the todos on the dom for the initial render.
  2. on clicking the todo text, the text color should turn to red
  3. add a new todo

HTML Markup

  1. We have an unordered list with a class of "todo-container" that will contain all our todos.
  2. Then we have three list items inside that "todo-container" and each individual list item has a class of "todo"
<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>
  <body>

     <ul class="todo-container">
      <li class="todo">Email team for update</li>
      <li class="todo">Answer tech team queries</li>
      <li class="todo">Send out meeting request</li>
    </ul>

    <script src="src/index.js"></script>
  </body>
</html>

JS Code

  1. Here we are selecting all the todos that has a class of "todo" using the querySelectorAll( ) method which will return us an array-like object (Nodelist) in which each entry will represent our todo list item element.

  2. On Nodelist, we can use forEach for iterating and can apply the same event listener on the individual to-do list items.

  3. Inside event listener we are just changing the color of todo text


const todos = document.querySelectorAll(".todo");

todos.forEach((todoElement) => {
  todoElement.addEventListener("click", function () {
    todoElement.style.color = "red";
  });
});

Here is the result

on clicking individual to-do list items, color is changing to red.

You might be thinking that adding an event handler on the individual elements is working fine, no need to change anything

but wait we have one feature left that is to add new todo, let's implement that feature also.

HTML Markup

  • We have a form with an id of "add-todo-form" which will be used to take new todo form the user.
<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>
  <body>

    <form id="add-todo-form">
      <input
        type="text"
        name="todo"
        id="todo"
        required
        placeholder="Add new todo"
      />
      <button>add</button>
    </form>

    <ul class="todo-container">
      <li class="todo">Email team for update</li>
      <li class="todo">Answer tech team queries</li>
      <li class="todo">Send out meeting request</li>
    </ul>

    <script src="src/index.js"></script>
  </body>
</html>

JS Code

  • To take input from users we are taking reference of add todo form and storing it in a variable named as addTodoForm.

  • To add a new todo in the todo list and show it in the view we are taking todos collection reference and storing it in a variable named as todosContainer.

  • On the form, a submit event is attached in which -

    • we are taking user input value.
    • creating a new list item element.
    • setting newly created list item innerText as user input value.
    • appending the newly created list item element on todosContainer.

const todos = document.querySelectorAll(".todo");
const addTodoForm = document.querySelector("#add-todo-form");
const todosContainer = document.querySelector(".todo-container");

todos.forEach((todoElement) => {
  todoElement.addEventListener("click", function () {
    todoElement.style.color = "red";
  });
});

addTodoForm.addEventListener("submit", function(e) {
  e.preventDefault();
  const newTodo = addTodoForm.todo.value;
  const newTodoElement = document.createElement("li");
  newTodoElement.innerText = newTodo;
  todosContainer.append(newTodoElement);
});

Here is the result

As you can see our change color to red functionality on clicking todo text is working for hardcoded todos, but on dynamically created todo it's not working.

REASON

In our case, the JavaScript file is executed from top to bottom and only once, after that only the code which is written in our event handlers is executed.

Let's try to implement this reason in our JS code.


// Selecting elements from the markup
const todos = document.querySelectorAll(".todo");
const addTodoForm = document.querySelector("#add-todo-form");
const todosContainer = document.querySelector(".todo-container");

// adding on click color change event on all todos
todos.forEach((todoElement) => {
  todoElement.addEventListener("click", function () {
    todoElement.style.color = "red";
  });
});

// taking user input and appending newly created todo element node
addTodoForm.addEventListener("submit", (e) => {
  e.preventDefault();
  const newTodo = addTodoForm.todo.value;
  const newTodoElement = document.createElement("li");
  newTodoElement.innerText = newTodo;
  todosContainer.append(newTodoElement);
});

as you can see from the flow of the code the code for adding an event listener on all todos is executed only once and because of that our dynamically created todo element is missing that event listener.

Now one thing we can do to solve this issue is that before appending the newly created list item element into todosContainer parent element we can attach our event listener on it.

code for that will look like this


addTodoForm.addEventListener("submit", function(e) {
  e.preventDefault();
  const newTodo = addTodoForm.todo.value;
  const newTodoElement = document.createElement("li");
  newTodoElement.innerText = newTodo;

// attaching event listener before appending
  newTodoElement.addEventListener("click", function () {
    newTodoElement.style.color = "red";
  });

  todosContainer.append(newTodoElement);
});

and here is the output

But we have a few more things to handle, you might have already noticed that we are repeating ourselves, and also if we have a large list of elements, each list item will have its own event handler and this is expensive because it takes a lot of memory.

To solve this issue event delegation comes into the picture

Event Delegation

By using event delegation we can solve both the issues-

  1. Saving memory by using the same event listener for all list items.
  2. Dynamically created elements will have that event handler because it is attached to the parent container, so no need to explicitly attach to each element.

But how to work with event delegation and achieve these 3 features?

  1. show all the todos on the dom for the initial render.
  2. on clicking the todo text, the text color should turn to red
  3. add a new todo

So for feature-1 and feature-3, there is no need to change our code. Refactor is required for feature-2 wherein previously we are attaching the event handler on individual elements.

But now we will attach that event handler on there common parent element that is todosContainer so let's do that first.

const addTodoForm = document.querySelector("#add-todo-form");
const todosContainer = document.querySelector(".todo-container");

// Event handler on common parent element
todosContainer.addEventListener("click", (e) => {
  console.log(e.target);
});

addTodoForm.addEventListener("submit", (e) => {
  e.preventDefault();
  const newTodo = addTodoForm.todo.value;
  const newTodoElement = document.createElement("li");
  newTodoElement.innerText = newTodo;
  todosContainer.append(newTodoElement);
});

as you can see in the above code the event handler is attached to the common parent element, but there is an issue with this code, as the event handler is on the parent element it will listen to the events occurred by both parent element as well as its child elements.

To make it work only for the child elements, we can use the event object that we receive in our event listeners callback function when an event has occurred.

The event object has multiple properties that we can use to uniquely identify the target element, in this example we will use tagName property

SYNTAX - event.target.tagName (returns tag name all uppercased on which that event has occured)


const addTodoForm = document.querySelector("#add-todo-form");
const todosContainer = document.querySelector(".todo-container");

// Event handler on common parent
todosContainer.addEventListener("click", function (e) {
  const targetElement = e.target;
  if (targetElement.tagName === "LI") {
    targetElement.style.color = "red";
  }
});

// To append new todo
addTodoForm.addEventListener("submit", function (e) {
  e.preventDefault();
  const newTodo = addTodoForm.todo.value;
  const newTodoElement = document.createElement("li");
  newTodoElement.innerText = newTodo;
  todosContainer.append(newTodoElement);
});

As you can see in the above code we are using the event object "e" for uniquely identifying the target element tag name and according to the tag name, we are doing some task, in this case, we are checking if the target element is a list item "LI", if it is changing its colour to red.

And since the event handler is attached to the common parent element todosContainer, whenever a new todo is appended into this container, it will have that functionality, so event delegation is handling that for us.

Here is the final code with event delegation implement

conclusion

  • Always use the event handlers according to your use case.
  • Never apply event handlers in a loop or list, use event delegation and apply on common parent instead.