JavaScript Event Handlers

by Nicklas Envall

An event is an indication that something has occurred. Events can be keypresses, mouse clicks, submitted forms, etc. We can respond to these fired events with event handlers. Event handlers are functions that run when their corresponding event gets triggered. What the event handler should do and which event it should respond to is up to us to define.

We can attach an event handler to an element in three different ways, HTML attribute, DOM property, and addEventListener. Before we look at how to attach an event handler, we should know that the element where the event occurs is known as the event target.

1. HTML attribute

You can attach your event handler via the on<event> attribute on an element’s HTML tag. We can only have one handler for each type of event.

<button onclick='eventHandler()'>Click me</button>

<script>
function eventHandler() {
    alert('hello');
}
</script>

2. DOM property

You can attach an event handler on the event target's property. The event properties are named like on<event>, for example, onclick, onmouseover, etc. With this approach, we also can only have one handler for each type of event, but we avoid mixing HTML and JS.

<button>Click me</button>

<script>
let btn = document.querySelector('button');

btn.onclick = function eventHandler () {
    alert('hello');
}
</script>

3. addEventListener

With addEventListener we can add as many event handlers to an event type as we’d like. We can distinguish an event handler and an event listener by thinking that the listener waits for the occurrence of the event, while the event handler contains the code that responds to the fired event.

<button>Click me</button>

<script>
let btn = document.querySelector('button');

// See how we can add more than one
btn.addEventListener('click', () => alert('hello'));
btn.addEventListener('click', () => alert('hello again'));
</script>

Event Object

When an event gets triggered, an event object will get created and passed to the event handler. The event object contains information about the event. It allows us to know more about our events, like the type of event and the event target:

btn.addEventListener('click', (event) => {
    console.log(event.type);
    console.log(event.target);
});

There are different types of events, which means we also have different types of event objects, such as:

  • MouseEvent Object
  • KeyboardEvent Object
  • DragEvent Object

There are many more different types of event objects, and these objects may contain different information. The reason why we have different types of event objects is that we usually want more specific information about that particular event type. For example, with a KeyboardEvent, we want to know which key the user pressed.

Even though there are different types of event objects, we should remember that they inherit properties and methods from "the" event object. One property we use a lot when dealing with all types of event objects is the target. event.target is a reference to the element where the event got triggered.

Event Propagation

Let us imagine we have a parent and child element that both have an event handler. Just like the following example:

<div>
    <button>Click me</button>
</div>

<script>
let div = document.querySelector('div');
let btn = document.querySelector('button');

div.addEventListener('click', () => alert('div'));
btn.addEventListener('click', () => alert('button'));
</script>

If we would click the button, then both event handlers would run, but in which order? The answer is that it depends on how we propagate the event through the DOM. It up to us to decide whether to use event bubbling, event capturing, or both.

Event Bubbling

Bubbling is when the event handlers get run starting from the target element and upwards. We call it bubbling because it bubbles up in the DOM tree like bubbles in a fizzy drink. So with bubbling, the example above will run alert('button') and then alert('div').

Event Capturing

Capturing is when the event handlers get run starting from the top moving downwards to the target element. It's like reverse bubbling. addEventListener chooses bubbling by default. We can tell addEventListener to use capturing instead by passing true as a third argument. So with capturing, the example above will run alert('div') and then alert('button').

Event Delegation

Event delegation allows us to use a single event handler for multiple elements. Imagine having many sibling elements that do the same thing on a click. It would be a waste to add an event handler for every element. We can avoid this with propagation, which allows us to delegate to an ancestor element's event handler instead. In the example below, we utilize event bubbling and only add an event handler to ul, but we can still handle each li with the help of event.target.

<ul>
  <li>one</li>
  <li>two</li>
  <li>three</li>
  <li>four</li>
</ul>

<script>
let ul = document.querySelector('ul');

ul.addEventListener('click', function (event) {
    if (event.target.tagName === 'LI') {
        alert(event.target.innerHTML);
    }
});
</script>

Cancelling Events

There are two methods you'll most likely encounter numerous times, stopPropagation and preventDefault. By using the method stopPropagation(), we can stop propagation. Only the button's event handler will be run in the example below:

<div>
    <button>Click me</button>
</div>

<script>
let div = document.querySelector('div');
let button = document.querySelector('button');

div.addEventListener('click', () => alert('div'));

button.addEventListener('click', (event) => {
    alert('button');
    event.stopPropagation();
});
</script>

By using the method preventDefault(), we can also stop some events default actions. In the example below the link will not work as expected for a user, because we prevent the default action:

<a href="https://en.wikipedia.org/wiki/JavaScript">JavaScript</a>

<script>
let a = document.querySelector('a');

a.addEventListener('click', (event) => {
    event.preventDefault();
});
</script>