JavaScript is a language, where events play a big role. In this article, we will talk about how they work. It includes different ways of listening to them and how they propagate. On the way we will cover some of mechanics from under the hood of JavaScript and browsers. We will also look a bit into the Event object. Let’s start!
What are events?
Events are actions that occur throughout our code. Situations in which user pressing a key on his keyboard, or scrolling his mouse happen in real time and with events we have a way to react to them. This is not limited to the user actions: there are many different events that can happen. A good example is an event that fires when the DOM is loaded.
Important thing is that JavaScript is asynchronous by nature. This makes events even more significant. Do not mistake that with multi-threading.
For the comparison of these two check out When async is not enough. An introduction to multithreading in the browser – it also covers concepts like call stack and event loop.
We can register functions that will act as handlers for specific events and as a result, they will be called when the event occurs. Browsers provide several ways to react to event notifications.
DOM attributes for on-event handlers
Attaching handlers to DOM elements by attributes can be used to define a way to react to certain events associated with the interface. The name of the attribute is `on${eventType}`. An example of that is onclick. The element that has the attribute will have the handler attached.
1 2 3 |
<button type="button" onclick="buttonClicked()"> Click </button> |
1 2 3 |
function buttonClicked() { console.log('Button clicked!'); } |
You can later refer to that attribute within your JavaScript code:
1 2 |
const button = document.querySelector('button'); console.log(button.onclick); // our function |
This approach has some serious downsides. The DOM element can have only one handler for a particular event type. The more important thing is that it reduces the readability of your code, because it involves writing JavaScript inside of your HTML structure. It simply doesn’t belong there.
EventTarget.prototype
It is a prototype that has a set of useful functions that allow us to handle events.
If you want to know more about prototypes, check out Prototype. The big bro behind ES6 class
Every DOM element (as well as the document and window object) inherits from EventTarget.prototype:
EventTarget.prototype.isPrototypeOf(Element.prototype); // true
It means that you can easily handle events that are connected to a particular element.
addEventListener
The method above attaches a function to be called whenever a specific event is delivered to the target. The first argument is the string describing the event type and the second one is the function to be called.
1 2 |
const button = document.querySelector('button'); button.addEventListener('click', buttonClicked); |
Note, that an object that this refers to here, is the button itself.
1 2 3 |
function buttonClicked() { console.log(this); // button } |
The first argument here is the Event object. If you would like to add more arguments or change the “this” binding, consider wrapping the callback in additional function:
1 |
button.addEventListener('click', (event) => buttonClicked(event, 'button')); |
If you would want to know more about what can “this” refer to, check out What is “this”? Arrow functions
Using addEventListener you can add more than one callback function. If you pass the same listener twice, it will not be called two times, though.
1 2 3 4 |
// function is not attached twice button.addEventListener('click', buttonClicked); button.addEventListener('click', buttonClicked); |
If you attach two separate functions, that will act identical, they will be called two times.
1 2 3 4 5 6 7 8 |
// function is attached twice! button.addEventListener('click', function () { console.log('button clicked'); }); button.addEventListener('click', function () { console.log('button clicked'); }); |
removeEventListener
You might want to keep some reference to the function that you used as a callback. This is because if at some point you don’t need a listener for an event anymore, you should remove it. It is a good practice to do that, keeping in mind the performance of the application. To do this, you need to pass the event type and the callback function to the removeEventListener function.
1 |
button.removeEventListener('click', buttonClicked); |
Additional options
When attaching the event listener, you can pass additional options as a third argument. This is also a great moment to talk about the mechanism of events some more.
capture
To understand this option, we first need to talk about the event propagation.
When an event occurs on an element with parent elements, browsers run two phases: capturing and bubbling.
In capturing phase:
- browser traverses all the ancestors of our target beginning with the most-outer one ( <html> element)
- if it finds an event handler of a matching type (for example “click”), it runs it
In the bubbling phase, the opposite happens:
- browser begins traversing the elements from our target, to the most further ancestor
- runs any matching event handler on the way
By default, our event handlers are attached to the bubbling phase:
1 2 3 4 5 |
<body> <div id="wrapper"> <button id="button">Click</button> </div> <body> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const wrapper = document.querySelector('#wrapper'); const button = wrapper.querySelector('#button'); button.addEventListener('click', function() { console.log('button was clicked'); }) wrapper.addEventListener('click', function() { console.log('wrapper was clicked'); }) document.body.addEventListener('click', function() { console.log('something was clicked'); }) |
After clicking on a button, you will see messages in that particular order:
button was clicked
wrapper was clicked
something was clicked
You can observe it even better using the Event object, that is passed to the callback.
1 2 3 4 5 6 7 |
button.addEventListener('click', function(event) { console.log(event.currentTarget === event.target); // true }) document.body.addEventListener('click', function(event) { console.log(event.currentTarget === event.target); // false }) |
The property called target is the actual target, that the caused the event. The currentTarget might not be the same as the original target due to the propagation.
It might be a little troublesome sometimes. With this being the case, you can use the stopPropagation function on your event.
1 2 3 4 |
button.addEventListener('click', function(event) { event.stopPropagation() console.log('Button clicked'); }) |
After doing that, the event will not propagate further up the DOM tree.
If you use the capture option, you will attach your listener in the capture phase instead:
1 2 3 4 5 6 7 8 9 10 11 |
button.addEventListener('click', function() { console.log('button was clicked'); },{ capture: true }) wrapper.addEventListener('click', function() { console.log('wrapper was clicked'); },{ capture: true }) document.body.addEventListener('click', function() { console.log('something was clicked'); },{ capture: true }) |
After clicking the button, messages will be presented in the following order:
something was clicked
wrapper was clicked
button was clicked
A useful methodology connected to that behaviour is event delegation. For a good example, visit David Walsh Blog.
once
With this boolean, you can indicate that the listener should be invoked not more than once. If you set it to true, it will be removed right after the invocation.
passive
Some DOM elements fire events by default. For example, a button with type “submit” (which is a default type), will submit the form:
1 2 3 4 |
<form id="nameForm"> <input type="text" name="firstname"/> <button>Click</button> </form> |
1 2 3 4 |
const form = document.querySelector('#nameForm'); form.addEventListener('submit', function(event) { console.log('The form is submitted'); }); |
You can prevent that from happening with the use of Event.prototype.preventDefault function:
1 2 3 4 |
const button = form.querySelector('button'); button.addEventListener('click', function(event) { event.preventDefault(); }); |
Now, even if the button is of type “submit”, it the form won’t be sent after the click.
If you add a passive option, you indicate that the callback will never call preventDefault – it would result in an error.
Unable to preventDefault inside passive event listener invocation.
This was introduced due to some scrolling performance issues.
Attaching the same callbacks with different options
Before, I wrote, that “if you pass the same listener twice, it will not be called two times”, but using the same callbacks with different options will result in separate listeners:
1 2 |
button.addEventListener('click', buttonClicked); button.addEventListener('click', buttonClicked, { capture: true }); |
The code above will cause the buttonClicked function to be called twice. This caused a need for the removeEventListener function to be able to accept arguments too. If they match with the options of the listener, it will be removed. The MDN docs state, that the only thing that needs to match is the value of the capture option, but browsers are inconsistent on this. Both on the newest Chrome and Firefox all the options needed to match.
useCapture
Before, we’ve only had one option, that we could pass to the listener. Because of that, there was just a boolean value sent, instead of the options object. It is the useCapture option, that acts just as a capture option mentioned above. To keep the backwards compatibility, you still can just pass a boolean value to the listener instead of an object and it will work just fine. If you are worried about older browsers, you might write a piece of code to check if other options are available.
Summary
In this article, we learned how the events flow through our applications. I hope that it helped you understand how they act by default and how can you alter this behaviour. Thanks to that you can debug the code faster, if any bugs connected to the events happen to emerge. Hopefully, it will even prevent them from happening in the first place. In the future, we might explore the how events work under the hood more, so stay tuned!