Complete JavaScript Course Module 9: Intermediate: Events and Forms

The Event Object, preventDefault and Event Delegation

  • Beginner
  • Lesson 9.2
  • 5 min read
  • 38 of 76 in the course

In short

Every event listener receives an event object full of useful details. In this lesson you will learn event.target, preventDefault, event bubbling and event delegation in JavaScript. What is the event object?

Every event listener receives an event object full of useful details. In this lesson you will learn event.target, preventDefault, event bubbling and event delegation in JavaScript.

What is the event object?

When an event happens, the browser calls your function and passes it one argument: the event object. People usually name it event or e. It describes what happened: which element was involved, which key was pressed, where the mouse was, and more.

Two properties are especially important:

  • event.target is the element where the event actually started, for example the exact thing that was clicked.
  • event.currentTarget is the element the listener is attached to.

Often they are the same. They differ when you click on a child inside the element that has the listener.

<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="card" style="padding: 20px; border: 1px solid #ccc;">
      <h3>Mountain bike</h3>
      <button>Add to cart</button>
    </div>

    <script>
      const card = document.querySelector("#card");

      card.addEventListener("click", (event) => {
        console.log("target:", event.target.tagName);
        console.log("currentTarget:", event.currentTarget.id);
      });
    </script>
  </body>
</html>
target: BUTTON
currentTarget: card

This is the output when you click the button. If you click the heading, target is H3. currentTarget is always the card, because that is where the listener lives. Note that tagName is in capital letters.

How to stop default browser actions with preventDefault

Some elements have a default action. Clicking a link opens a new page. Submitting a form reloads the page. Calling event.preventDefault() tells the browser: "Don't do your normal thing; I will handle it."

<!DOCTYPE html>
<html lang="en">
  <body>
    <a href="https://example.com" id="leave">Go to example.com</a>

    <form id="search">
      <input name="q" type="search" placeholder="Search recipes">
      <button>Search</button>
    </form>
    <p id="result"></p>

    <script>
      const link = document.querySelector("#leave");
      link.addEventListener("click", (event) => {
        const sure = confirm("You have unsaved changes. Leave anyway?");
        if (!sure) {
          event.preventDefault();
          console.log("Navigation cancelled");
        }
      });

      const form = document.querySelector("#search");
      form.addEventListener("submit", (event) => {
        event.preventDefault();
        const query = form.elements.q.value;
        document.querySelector("#result").textContent = `Searching for "${query}"...`;
      });
    </script>
  </body>
</html>
Navigation cancelled

If you click the link and choose Cancel, you stay on the page. Typing "soup" and submitting the form shows Searching for "soup"... without a page reload. You will use preventDefault in almost every form you handle with JavaScript.

Event bubbling explained simply

When you click a button, the click does not only happen on the button. It also "bubbles up" to the button's parent, then the grandparent, and so on, all the way to document. It is like a bubble rising in water through every layer above it.

click on <button>
   ↑ bubbles to <li>
   ↑ bubbles to <ul>
   ↑ bubbles to <body>
   ↑ bubbles to document

That is why, in the first example, clicking the button also ran the listener on the card. Bubbling sounds strange at first, but it is very useful, as you will see in a moment.

You can stop an event from bubbling further with event.stopPropagation(). You will rarely need it. Stopping propagation can break other code that listens higher up, such as analytics or a "close menu when clicking outside" feature.

Note: preventDefault() and stopPropagation() do different jobs. The first stops the browser's default action (like following a link). The second stops the event from reaching parent elements. One does not do the other.

Event delegation for dynamic lists

Imagine a list where each item has a "Remove" button, and new items can be added at any time. You could add a listener to each button, but new buttons would not have one. Event delegation solves this. You put one listener on the parent, and thanks to bubbling it hears clicks from all children, even ones added later. Then you check event.target to see what was clicked.

The closest() method helps here. element.closest(selector) looks at the element itself and then up through its parents, and returns the first one that matches the selector (or null). It is useful when the button contains an icon or a <span>, because the click target might be the inner element.

<!DOCTYPE html>
<html lang="en">
  <body>
    <ul id="cart">
      <li data-name="Coffee">Coffee <button class="remove"><span>✕</span> Remove</button></li>
      <li data-name="Tea">Tea <button class="remove"><span>✕</span> Remove</button></li>
    </ul>
    <button id="add">Add cookies</button>

    <script>
      const cart = document.querySelector("#cart");

      cart.addEventListener("click", (event) => {
        const button = event.target.closest(".remove");
        if (!button) return;

        const item = button.closest("li");
        console.log(`Removed ${item.dataset.name}`);
        item.remove();
      });

      document.querySelector("#add").addEventListener("click", () => {
        const li = document.createElement("li");
        li.dataset.name = "Cookies";
        const removeButton = document.createElement("button");
        removeButton.className = "remove";
        removeButton.textContent = "Remove";
        li.append("Cookies ", removeButton);
        cart.append(li);
      });
    </script>
  </body>
</html>
Removed Tea
Removed Cookies

Click "Remove" next to Tea and it disappears. Then click "Add cookies" and remove the new item: it works too, even though that button did not exist when the listener was added. If you click the ✕ icon, event.target is the <span>, but closest(".remove") still finds the button. If you click on empty space in the list, closest returns null and we return early.

Tip: Event delegation also means fewer listeners. One listener on a list of 1,000 items is lighter than 1,000 separate listeners.

Practice exercise

Make a list of five colour names. Put one click listener on the <ul>. When a list item is clicked, give it the class selected and remove selected from any other item, then log the colour name. Add a button that adds a new colour to the list and check that clicking the new item also works.

Hint: Use event.target.closest("li") to find the clicked item, and list.querySelector(".selected") to find the currently selected one. It may be null the first time, so check before removing its class.

Key takeaways

  • Listeners receive an event object; event.target is where the event started, event.currentTarget is where the listener is attached.
  • event.preventDefault() stops default actions like following a link or submitting a form.
  • Events bubble up from the target through all its parents.
  • Event delegation uses one listener on a parent to handle clicks on many children, including new ones.
  • closest(selector) finds the nearest matching element going upward; use stopPropagation() only when you really must.