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.

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

functionbuttonClicked(){

console.log('Button clicked!');

}

You can later refer to that attribute within your JavaScript code:

1

2

constbutton=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.

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

constbutton=document.querySelector('button');

button.addEventListener('click',buttonClicked);

Note, that an object that this refers to here, is the button itself.

1

2

3

functionbuttonClicked(){

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:

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

constwrapper=document.querySelector('#wrapper');

constbutton=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

constform=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

constbutton=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.

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!