An intro to events in the DOM
Table of Contents
- Event Driven Architecture
- First, an example:
- Event type, handlers
- The
event
object - Good to know, not to use: inline handlers
- Event Propagation
- Event Delegation
- Removing Event Listeners
So far, the code that we have written will be executed, one line after the other.
console.log('a')
console.log('b')
console.log('c')
In event-driven architecture, some of the code that we write will only be executed in response to a user event such as:
- clicking a button
- moving your mouse
- using your keyboard
// 1. select the element that will be the "target" of the event
// 2. define an event handler callback
// 3. make the button to "listen" for "click" events and respond by invoking the event handler
const button = document.querySelector('button');
const handleClick = (event) => {
console.log('a click occurred!');
}
button.addEventListener('click', handleClick);
An element is programmed to "listen" for these events.
An event handler is the function that is invoked when an event "fires".
See this in action in the
0-basic-examples/
website.
When you use Element.addEventListener()
method, the first argument defines the event type which may be one of:
click
mousemove
keydown
keyup
submit
- many more!
The second argument is an Event Handler, a callback function that is invoked when the event fires.
// a generic event handler
const eventHandler = () => console.log('an event occurred!');
// register an event listener on a button
document.querySelector('button').addEventListener('click', eventHandler);
// register these event listeners on the entire document
document.addEventListener('mousemove', eventHandler);
document.addEventListener('keydown', eventHandler);
When any event handler is invoked, it is given an event
object as an argument. The event
object has about the event, with different properties for different event types.
// this event handler ignores the event argument
const eventHandler = () => console.log('an event occurred!');
// this event handler actually uses it!
const printEvent = (event) => console.log(event);
// we can assign multiple event handlers to an element
document.querySelector('button').addEventListener('click', eventHandler);
document.querySelector('button').addEventListener('click', printEvent);
Here are some essential event
properties:
event.target
— the Element that triggered the event. Available for all eventsevent.key
— the key pressed. Available for keyboard events.event.x
/event.y
— the location of the mouse in the window. Available for mouse events. (alias for.clientX
/.clientY
)
See this in action in
turtle-walker/
website.
You can also define event handlers inline directly in HTML:
<button onclick="console.log('hello world');">I have an inline handler!</button>
This is good to be aware of for when we get to React but you should NOT use this.
The key concept to understand is that child events can affect parent elements.
"Event Propagation" is just a fancy way of saying “clicking on a child element affects the parents”
When an event is triggered by a child, it is detected by the parent.
<div id="outer">
<div id="middle">
<button id="inner">Click me!</button>
</div>
</div>
Two values to be aware of:
event.target
(the Element that fired the event)event.currentTarget
(the Element handling the event)
To prevent events from bubbling up, use event.stopPropagation()
const testPropagation = (event) => {
console.log(`Event detected on #${event.target.id}`);
console.log(`Event handled by: #${event.currentTarget.id}`);
// event.stopPropagation()
}
document.querySelector('#outer').addEventListener('click', testPropagation);
document.querySelector('#middle').addEventListener('click', testPropagation);
document.querySelector('#inner').addEventListener('click', testPropagation);
In this example, because #inner
is a child of #middle
and #outer
, clicking on #inner
will trigger its own event handler as well as the event handlers of #middle
and #outer
.
Q: What would happen if we removed the event handlers for #inner
and #middle
?
Event delegation is the idea that you can have a single event listener on a parent element that can handle events for all of its children.
This is useful for child elements that are added/removed dynamically. Like when a user adds a new item to a list.
<ul id="counting-list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
This example has the ul
listening for clicks on the child li
Elements. If a click is detected, the number of the li
that was clicked on is saved and a new li
is appended to the list with that number plus 1.
const ul = document.querySelector('#counting-list');
ul.addEventListener('click', (event) => {
if (event.target.matches('li')) {
const numberOfLiClicked = Number(event.target.innerText);
const li = document.createElement('li');
li.innerText = numberOfLiClicked + 1
event.currentTarget.append(li);
}
});
Because the new li
Elements are also children of the ul
, the new li
Elements are also clickable! We don't need to add additional event listeners for each new li
because the ul
will handle it.
Here's the pattern:
- Grab a parent element
- Have it listen for events, it will detect events triggered by its children because of propagation
- Identify the target to decide what you want to do using
event.target.matches()
event.target.classList.contains()
is also useful for identifying the target.
One of the reasons why passing a named callback function to your listeners is better is because you can then remove them if you need to.
const handleCountClick = (e) => {
e.target.dataset.count++;
e.target.innerText = e.target.dataset.count;
};
const counterButton = document.querySelector("#counter");
counterButton.addEventListener('click', handleCountClick);
const removeListenerButton = document.querySelector("#remove-listener");
removeListenerButton.addEventListener('click', (e) => {
// To remove an event listener, provide the event type and the handler
counterButton.removeEventListener('click', handleCountClick);
})
We remove event listeners to limit user interactions and also be 100% sure that we aren't committing memory leaks when we remove elements. (However, modern browsers are pretty good at cleaning up after us).
Q: Why can we write the removeListenerButton
event listener as an inline arrow function but we can't for the counterButton
event listener?