Skip to the content

Handling events with addEventListener

Last updated: By Lukas BrandtLesson 3 of 114 min read

So far every script has run once, top to bottom, the moment the page loaded. Real pages wait for something to happen: a click, a key press, a form being sent. Each of those is an event, and addEventListener is how you tell an element what to do when one occurs.

Your first listener

This builds on the bench-notes page and the class switching from the lesson on changing CSS. Add one rule to the stylesheet:

<style>
  .notes-hidden .note { display: none; }
</style>

Then the script:

const button = document.querySelector('#toggle-notes');

function toggleNotes() {
  document.body.classList.toggle('notes-hidden');
}

button.addEventListener('click', toggleNotes);

Click the button and the notes disappear; click again and they return. The class goes on <body>, and the CSS rule hides every .note inside it, so one class change controls all three paragraphs.

addEventListener takes two arguments. The first is the event type as a string, without any "on" in front: 'click', 'input', 'submit', 'keydown'. The second is the function to run, passed by name with no parentheses after it. You are handing the browser the function itself, and the browser calls it later, every time the event happens.

The event object

When the browser calls your function, it passes one argument: an object that describes what happened.

function report(event) {
  console.log(event.type);           // "click"
  console.log(event.target);         // the element that was clicked
  console.log(event.currentTarget);  // the element the listener is on
}

document.body.addEventListener('click', report);

Click the button and target is the button while currentTarget is the body. That works because events bubble: after firing on the element you clicked, a click travels up through each ancestor, so a listener on <body> hears clicks from anything inside it. The relational properties lesson turns that into a useful pattern.

Different events carry different details. Keyboard events have event.key ('Enter', 'a', 'Escape'), and mouse events have coordinates such as event.clientX.

Typing and submitting

The input event fires every time the value of a text field changes, which makes a live preview a few lines long:

<form id="note-form">
  <label>New note <input id="note-text" type="text" maxlength="80"></label>
  <button type="submit">Add</button>
</form>
<p id="preview"></p>
const field = document.querySelector('#note-text');
const preview = document.querySelector('#preview');
const form = document.querySelector('#note-form');

function updatePreview() {
  preview.textContent = field.value;
}

function handleSubmit(event) {
  event.preventDefault();
  console.log('Would save:', field.value);
  form.reset();
  preview.textContent = '';
}

field.addEventListener('input', updatePreview);
form.addEventListener('submit', handleSubmit);

Listen for submit on the form rather than click on its button. Pressing Enter inside the text field submits the form too, and only the submit listener catches both routes. event.preventDefault() cancels the browser's default action, which for a form means sending the data and loading a new page. Leave it out and the page reloads before you can read the console.

Taking a listener off again

removeEventListener needs the same event type and the very same function you passed in:

field.removeEventListener('input', updatePreview);

For a listener that should only ever run once, pass an options object as a third argument instead: button.addEventListener('click', toggleNotes, { once: true }). The browser removes it after the first call.

Common mistakes

  1. Calling the function instead of passing it. button.addEventListener('click', toggleNotes()) runs toggleNotes right away, once, and hands its return value (undefined) to the browser as the listener. Drop the parentheses.
  2. Writing 'onclick' as the type. The string is 'click'. The "on" prefix belongs to the old HTML attributes, and a listener for 'onclick' simply never fires.
  3. Selecting an element that does not exist yet. If the script runs before the markup, querySelector returns null and Chrome reports "Cannot read properties of null (reading 'addEventListener')". Firefox words it as "button is null". The next lesson fixes the timing for good.
  4. A typo in the selector. Same null, same error. Check for # before ids and . before classes.
  5. Calling addEventListener on a querySelectorAll result. A NodeList has no such method, so you get "is not a function". Attach a listener to each element instead; the loops lesson shows how.
  6. Removing a function written inline. Each function written directly inside the call is a brand-new function, so removeEventListener has nothing to match. Give a function a name if you will ever need to remove it.
  7. Using JavaScript for a pure hover effect. A color change on hover is a job for CSS, covered in the :hover lesson. Save mouseover for behavior, such as showing a hint that needs data from the script.

The old ways

You will meet onclick="toggleNotes()" written into HTML and button.onclick = toggleNotes in scripts. Both still work. The attribute mixes behavior into your markup, though, and the property holds only one function: assign a second one and the first is silently replaced. With addEventListener, several listeners can share one event without knowing about each other.

Parts links can earn us a commission. As an Amazon Associate I earn from qualifying purchases.

Try it

Open the online HTML editor, paste the form and its script, and add a third listener that logs event.key for keydown on the text field. Type a few letters, press Backspace, then press Enter, and compare what the console prints with what appears in the preview. Backspace shows up as a key but changes the preview only when there was text to delete.