Lesson 308H.13 - Event Handling

Learning Objectives

By the end of this lesson, learners should be able to:

  • Add event listeners for events such as click.
  • Explore the event object.
  • Explain event bubbling and capturing.
  • Use event bubbling to implement event delegation.
  • Stop an event from bubbling.

 CodeSandbox

This lesson uses CodeSandbox as one of its tools.

If you are unfamiliar with CodeSandbox, or need a refresher, please visit our reference page on CodeSandbox for instructions on:

  • Creating an Account
  • Making a Sandbox
  • Navigating your Sandbox
  • Submitting a link to your Sandbox to Canvas

DOM Events

DOM events are the bedrock of interactivity on web pages. DOM events enable us as developers to implement event-driven programming. This programming paradigm is such that much of our code runs in response to events being triggered during run-time.

Lots of events are being generated within the browser. For example, an event (or multiple events) is created when:

  • A user moves or clicks the mouse.
  • A user presses a key.
  • A form is submitted or its inputs modified.
  • The page has finished loading or has been resized.
  • etc.

Take a look at the MDN documentation on event to see the sheer number of events and event types.


Event Listeners

An event listener is a function, more specifically, a callback function, that is called when an event fires.

Event listeners may also be referred to as event handlers.

There are three different approaches for registering event listeners:

  • In the HTML (inline): <button id="reset-btn" onclick="reset()">.
  • Assigning to DOM elements' properties: resetBtn.onclick = reset;.
  • Calling addEventListener() on a DOM element.

Using the HTML approach (onclick="reset()") is typically frowned upon because it requires that the function be in the global scope. In addition, this, like inline styling, kind of breaks the separation of concerns design principle.

The DOM element approach (resetBtn.onclick = reset;) is better because the function does not have to be in global scope, however...

The addEventListener approach is widely considered to be the best practice because it has the flexibility of adding multiple listener functions!

Here is the common syntax for registering an event listener for a given event:

element.addEventListener(<event-name>, <callback>, <use-capture>);
  • event-name is the name of the event (string).
  • callback is the function we want executed when the event happens. When called by the JS engine, it will be passed an event object as an argument.
  • use-capture is a boolean and is optional. It has to do with event phases. We won't need to worry about it here, but if you want to know more read the Event Phases section of this article.

Event Listener Exercise

We're going to create a small application together to familiarize ourselves with event listeners:

  • Create a folder named events-practice and cd into it.
  • Create an index.html and use ! [tab] to create the HTML boilerplate.
  • Make a js folder and a js/main.js file.

Add a <script> tag to include main.js in the <head> of our index.html:

<head>
	...
	<title>Events Practice</title>
	<script defer src="js/main.js"></script>
</head>

The defer attribute ensures the DOM is ready before the script executes.

Putting a temporary alert('js loaded) into our script file will verify main.js is being loaded.

After setting up the project, add the following HTML:

<h3>Comments</h3>
<ul>
	<li>Testing Events!</li>
</ul>
<input>
<button>Add Comment</button>
  • When we click the Add Comment button, we want to create a new comment with the text entered in the input and then clear the input.
  • We can add a click event listener to pretty much any element - not just buttons. However, buttons are pre-styled to look and act clickable.

We're going to use an anonymous callback function in this first example:

const btn = document.querySelector('button');
btn.addEventListener('click', function(evt) {
	// testing!
	console.log(evt);  
});
  • If all goes well, clicking the button should log out the event object.
  • Congrats, registering an event listener is that easy!

The Event Object

Examining the event object that was provided as an argument to our event listener reveals lots of useful information about the event!

Of special interest are:

  • Several ...X and ...Y properties that provide where the click occurred.
  • The target property, which holds a reference to the DOM element that triggered (dispatched) the event.
  • The JS's this keyword within the listener function, which will be set to the DOM element that addEventListener was called on.

Knowledge Check

❓ What is the name of the method used to attach event listeners to elements?

❓ What is that method's signature (a method's name, the number & type of arguments it takes, and what it returns)?

❓ Name three events that might be triggered in the browser.


Creating a New <li> Element

Continuing upon our example exercise, if we want to add a new comment we're going to need to create a new <li> element.

Here's how we can do it using the document.createElement method:

btn.addEventListener('click', function(evt) {
	const li = document.createElement('li');
	console.log(li)
});

Note: At this point, the element is "in memory" only and is not part of the DOM (yet).

Okay, we have a new <li> element created and assigned to a variable named li, but it has no content.

We want to get whatever text the user has typed into the <input> element.

Using our <input> element's value, we can set the textContent of the new <li>:

btn.addEventListener('click', function(evt) {
	const li = document.createElement('li');
	const inp = document.querySelector('input');
	li.textContent = inp.value;
});

Now the new <li> is ready to be added to the DOM!

  • Which element do we we want to add the <li> to?

There are several ways to add DOM elements to the document using JavaScript, as previously discussed.

A common way to add new elements to another element is by using the appendChild method like this:

	li.textContent = inp.value;
	// new code below
	document.querySelector('ul').appendChild(li);
});

Note that the new element is appended as the last child.

Test it out - nice! The new comment has been added, but if we want to improve the UX, we have one more task - clear out the <input>.

One line of code is all it takes - try to solve it yourself!


Event Bubbling

When an event occurs on an element, that event, whether it is listened to on that element or not, bubbles up through the DOM, all the way up to the document object.

All event listeners registered for the same event, such as click, will be invoked along the path to the document element - unless one of those listeners calls the event object's stopPropagation method, which we'll touch on later.

Why does JS bubble up (propagate) its events? Event delegation.


Event Delegation

Imagine a web app, like a game perhaps, with lots of elements that need to respond to a click. It's possible there could be tens, hundreds, or more of these elements.

That would be a lot of listeners, wouldn't it? Not very efficient at all. Additionally, every time a new element is added, the event listener would also have to be registered!

Event bubbling allows us to implement what's known as event delegation. Event delegation allows us to register a single event listener that can respond to events triggered by any of its descendants. Much more efficient!

Let's register a listener within our example project (this time for kicks we'll use a named function) on the <ul> that can respond to clicks on any of its <li>s:

document.querySelector('ul')
	.addEventListener('click', handleClick);

function handleClick(evt) {
	console.log(evt);
}

Importantly, the event object's target property is set to the actual element that was clicked! This allows us to test for specific elements, and run conditional logic.

document.querySelector('ul')
	.addEventListener('click', handleClick);

function handleClick(evt) {
	if (evt.target.id == 'specialListItem') {
		// do something special
	}

	console.log(evt);
}

Not only is event delegation more efficient by it's very design, it's dynamic. As descendants are added, they too will be listened to!

Without event delegation, you would have to register a listener every time a new element is added, such as each <li> element in our example.


Exercise

Write the code to change the color of the text of a clicked comment in our practice project.


Stopping Event Propagation

All of that said, event bubbling can sometimes cause headache. The Event interface includes a stopPropagation method which will prevent an event from bubbling up into any other elements.

When you have elements nested within other elements and many events being listened to, as is often the case in development, you'll want to keep an eye on how your events bubble, and use stopPropagation when necessary to prevent erroneous event handling.

Let's look at an example of this in action within this CodeSandbox.

  • Click on the button.

    • Notice that despite clicking on the button, the click event "bubbled" up and registered on the div and body elements as well, which may not be intended behavior.
  • Click slightly to the right of the button.

    • Notice that here it only registers the div and body elements being clicked.
  • Add a stopPropagation call within the event handler. What happens?

Event Capturing

You can also reverse the process of event bubbling, which is called event capturing. When the { capture: true } option is passed into addEventListener, the event fires first on the least nested element, and then on successively more nested elements until the target is reached. Event capturing is disabled by default.

Let's explore this CodeSandbox to see the differences between bubbling and capturing. This example is identical to the earlier example, except with { capture: true } passed to addEventListener.

  • Click around and observe the results.
  • See what happens if you add stopPropagation.

Most of the time, event bubbling is the more useful tool.


Default Browser Actions

Many events have default browser actions associated with them. For instance, clicking on a link initiates navigation to that link and submitting a form initiates communication with a server. In many cases, this default behavior runs contrary to what we're trying to accomplish. Luckily, the Event object has a preventDefault() method that can be used to stop the default behavior from occuring.

The power of preventDefault cannot be understated, and is limited only by your creativity. Let's take a look at an example provided by the MDN documentation on preventDefault that shows how a form input can be restricted by preventing the default keypress action.

Visit this CodeSandbox to explore the example as a class.


Removing Event Listeners

It's possible to remove an added event listener; however, only if a named function was used as the callback:

btn.removeEventListener('click', handleClick);

This would remove the 'click' event listener (handleClick) that was registered on the btn element like this: btn.addEventListener('click', handleClick);.


Knowledge Check

❓ What is the argument that JS passes to an event listener when it calls it?

❓ What is the name of the property on the above argument that represents the DOM element that dispatched the event?

❓ Let's say you needed to have an event listener respond to a click event on the <td>s within a <table> - would you have to add event listeners to each <td>? Support your answer.


Practicing DOM Manipulation (Part Two)

Now that you've got a handle on event handling, complete R-ALAB 308H.13.1 - DOM Manipulation (Part Two), which expands upon the lab activity that you completed earlier by adding elements with events.

Copyright © Per Scholas 2024