I am creating this post as notes from Wes Bos Javascript course which you can sign up for and do with me here: https://beginnerjavascript.com/. Here is a link to Wes' GitHub readme.md.
This Post specifically follows Module #5, slides 29-32 for reference. It's lengthy, but I sincerely hope it helps clarify some of the course work and you can use it to follow along with Wes' instructions. It follows pretty closely to the topics/discussions with additional notes here and there. Let's get into it!
What's an event?
Let's learn about events, event listeners, and how to listen for them and how to do stuff when stuff happens.
DOM elements (things on the page) they emit events when they are interacted with. We can use event listeners to react to them! You can attach event listeners to all elements as well as the document and the window.
How To Add An Event
So there are three steps:
- Go get something (selecting)
- Listen for something.
- Do something
In order to listen to events in javascript, you first have to select the element - if you aren't familiar with how to do that, check out this DOM Manipulation section on selecting.
What are the types of events you can listen to? The most common one you will see is a click
event. We are going to look at this one first and it looks like this:
object.addEventListener('click', anonFunction(){
alert('You Clicked An Object!');});
.
You can create a function outside of this function and call it inside the anonFunction above or instead of the anonFunction. No need to immediately run it with
()
or it will run on page load.
How To Remove An Event
To remove an event you will need to unbind
it (Binding is taking a function and listening for a specific click against the element, unbinding is the opposite). For specificity, the way you unbind it
you will need to run something like: object.removeEventListener(event, function);
Note: to remove a listener, you will have to use a named function - anonomous functions cannot be removed.
Listening To Events On Multiple Items
You can't just take an array of elements to select (create a node array). Let's look at an example:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>JavaScript Events</title> <link rel="stylesheet" href="../../base.css"> </head> <body> <button class="butts">Click Me!</button> <button class="cool">Click me also!</button> <h2>Buy Buttons!</h2> <button class="buy">Buy Item 1</button> <button class="buy">Buy Item 2</button> <button class="buy">Buy Item 3</button> <button class="buy">Buy Item 4</button> <button class="buy">Buy Item 5</button> <button class="buy">Buy Item 6</button> <button class="buy">Buy Item 7</button> <button class="buy">Buy Item 8</button> <button class="buy">Buy Item 9</button> <button class="buy">Buy Item 10</button> <img class="photo" src="https://picsum.photos/200" alt="Nice"> <script src="./events.js"></script> </body> </html>
Adding an event listener 10 times would not be DRY or efficient. Let's try using a querySelectorAll
and see what happens.
const buyButtons = document.querySelectorAll('button.buy');
That line of code above would select all the buttons on lines 16-25. When trying to add an event listener like the following to those selected elements:
buyButtons.addeventListener('click', buyItem);
This will result in an error that looks like this:
In order to attach event listeners to multiple elements, first, you will need to loop through all the elements that you selected like this:
// 1. Select const buyButtons = document.querySelectorAll('button.buy'); //selected // 2. Listen & 3. Do buyButtons.forEach(function(eachButton){ // anon function console.log('Binding the buy button;); //should run 10 times buyButton.addEventListener('click', buyItem); });
To remove the event listener for multiple items, you would need to loop over the entire node array again.
Another way to do this would be to do it without an anonymous function, more specifically with a named function like this which will be binded
:
// 1. Select const buyButtons = document.querySelectorAll('button.buy'); //selected // 2. Listen function attachListener(doesntMatterTheName){ //named function console.log('Binding the buy button;); //should run 10 times doesntMatterTheName.addEventListener('click', buyItem); }; // 3. Do buyButtons.forEach(attachListener);
The Event Object
The event object is a gold mine of information of what happens when an event
Wes Bos"goes"fires.
Parameters
Before diving in here, we need to look back at parameters.
When you pass a parameter in the event listener, it's just a place holder. You can use anything you'd like because the browser will determine what goes in there. It's just a named variable passed into a function to import arguments. The difference between an argument
and a parameter
is parameters:
- Names listed in the definition of the function.
- Arguments are the real values passed into the function.
- Parameters are initialized to the values of the arguments supplied.
There are two kinds of Parameters:
1. input parameters - They pass values into functions
2. output/return parameters - These primary return multiple values from a function but aren't recommended since they can cause confusion.
/sauce
Event Listener Callback
The event listener can be specified as either a callback function or an object that implements EventListener
, whose handleEvent()
method serves as the callback function.
The callback function itself has the same parameters and return value as the handleEvent()
method; that is, the callback accepts a single parameter: an object based on Event
describing the event which has occurred, and it returns nothing.
For example, an event handler callback that can be used to handle both fullscreenchange
and fullscreenerror
might look like this:
function eventHandler(event) { if (event.type == 'fullscreenchange') { /* handle a full screen toggle */ } else /* fullscreenerror */ { /* handle a full screen toggle error */ } }
What you can see in the above is event
is a callback parameter. This can literally be named anything you like.
PointerEvent (1 word)
Let's look at our real example from the previous section.
const buyButtons = document.querySelectorAll('button.buy'); function attachListener (event) { //remember event can be called anything console.log('you are buying it'); console.log(event); }; buyButtons.forEach(function(buyButton){ buyButton.addEventListener('click', attachListener);
If you run this, in the console log you will see something like this:
What is a PointerEvent
you might ask? It gives you all kinds of details about the action taken on the click event. Think 3d touch
for iOS for example, or whether you are creating a game and want someone to not be able to programmatically create click events to cheat, or reCaptcha. All the details are located within this PointerClick
.
There is the treasure trove of details here. But what we are focused on is the target method.
Targets
If we were to edit the previous example, we will actually see in the console log what element the user clicked on.
const buyButtons = document.querySelectorAll('button.buy'); function attachListener (event) { //remember event can be called anything console.log('you are buying it'); console.log(event.target); // ***** LOOK HERE ****** }; buyButtons.forEach(function(buyButton){ buyButton.addEventListener('click', attachListener);
This is really helpful because we can go to the HTML and add data-
attributes to our buttons.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>JavaScript Events</title> <link rel="stylesheet" href="../../base.css"> </head> <body> <button class="butts">Click Me!</button> <button class="cool">Click me also!</button> <h2>Buy Buttons!</h2> <button data-price="100" class="buy">Buy Item 1</button> <button data-price="200" class="buy">Buy Item 2</button> <button data-price="300" class="buy">Buy Item 3</button> <button data-price="400" class="buy">Buy Item 4</button> <button data-price="500" class="buy">Buy Item 5</button> <button data-price="600" class="buy">Buy Item 6</button> <button data-price="700" class="buy">Buy Item 7</button> <button data-price="800" class="buy">Buy Item 8</button> <button data-price="900" class="buy">Buy Item 9</button> <button data-price="1000" class="buy">Buy Item 10</button> <img class="photo" src="https://picsum.photos/200" alt="Nice"> <script src="./events.js"></script> </body> </html>
We can then console.log
that dataset and pull the price. Let's check the example:
const buyButtons = document.querySelectorAll('button.buy'); function attachListener (event) { //remember event can be called anything console.log('you are buying it'); console.log(paseFloat(event.target.dataset.price)); // ***** LOOK AT The LINE ABOVE ****** }; buyButtons.forEach(function(buyButton){ buyButton.addEventListener('click', attachListener);
Remember
parseFloat()
keeps decimals,parseInt()
doesn't
Now every time you click on a button, you will see the following information in the console log:
Recapping: the event methods allows you to pull a ton of different information about the event you called.
event.target vs event.currentTarget
They are the ===
but the difference is when the attributes nested inside are different. If we console.log(event.target);
, console.log(event.currentTarget);
and console.log(event.target === event.currentTarget);
And click on the button anywhere it will result as true.
BUT! If you were to wrap part of the HTML in the button with a strong
tag like below, and then try clicking on the Buy Item
text in the button, the result would be true. But what happens when you click the number?
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>JavaScript Events</title> <link rel="stylesheet" href="../../base.css"> </head> <body> <button class="butts">Click Me!</button> <button class="cool">Click me also!</button> <h2>Buy Buttons!</h2> <button data-price="100" class="buy">Buy Item <strong>1</strong></button> <button data-price="200" class="buy">Buy Item <strong>2</strong></button> <button data-price="300" class="buy">Buy Item <strong>3</strong></button> <button data-price="400" class="buy">Buy Item <strong>4</strong></button> <button data-price="500" class="buy">Buy Item <strong>5</strong></button> <button data-price="600" class="buy">Buy Item <strong>6</strong></button> <button data-price="700" class="buy">Buy Item <strong>7</strong></button> <button data-price="800" class="buy">Buy Item <strong>8</strong></button> <button data-price="900" class="buy">Buy Item <strong>9</strong></button> <button data-price="1000" class="buy">Buy Item <strong>10</strong></button> <img class="photo" src="https://picsum.photos/200" alt="Nice"> <script src="./events.js"></script> </body> </html>
The result would be false
. The difference is when you use event.target
, it is from the thing that actually got clicked whereas event.currentTarget
will be the "thing" that fired the event. For most cases, you will want to use event.currentTarget
.
Bubbling (Propagation)
Here is where things get interesting:
What happens when we add an event listener to the window
and click around on different elements?
const buyButtons = document.querySelectorAll('button.buy'); function attachListener (event) { //remember event can be called anything console.log('you are buying it'); console.log(paseFloat(event.target.dataset.price)); console.log(event.target); console.log(event.currentTarget); console.log(event.target === event.currentTarget); }; buyButtons.forEach(function(buyButton){ buyButton.addEventListener('click', attachListener); window.addEventListener('click', () => { console.log('YOU CLICKED THE WINDOW'); };
If you click on the blue background, you get a console.log of YOU CLICKED THE WINDOW
.
If you click on the H2
, you get a console.log of YOU CLICKED THE WINDOW
.
If you click on the number
inside the strong tag on the button, are you clicking on the window
, or the button
number?
Both fire off. This is referred to as propagation. Basically, when you click on the strong tag the event bubbles up in case anything else was listening on the event (in this case the body
, the HTML
, and the window
, and the browser
.
The way you can stop that is by adding something like this:
const buyButtons = document.querySelectorAll('button.buy'); function attachListener (event) { //remember event can be called anything console.log('you are buying it'); console.log(paseFloat(event.target.dataset.price)); console.log(event.target); console.log(event.currentTarget); console.log(event.target === event.currentTarget); //************ THIS STOPS THE EVENT FROM BUBLING UP ************* event.stopPropagation(); //************ UP THERE ********* }; buyButtons.forEach(function(buyButton){ buyButton.addEventListener('click', attachListener); window.addEventListener('click', () => { console.log('YOU CLICKED THE WINDOW'); };
Now if you click on the number within the strong tag
it will not also say "YOU CLICKED THE WINDOW" in the console log.
Recap: bubling
or more accurately propagation
is stopped by using event.stopPropagation();
Capture
Capture
is sort of the opposite of propagation. Here is what I mean.
If you have an element very low in the DOM tree - what happens when someone makes a click, you are actually clicking on the document
, html
, body
, tabletbody
, tr
, then the td
tag. It doesn't exactly do anything but it's kind of like a diary of everywhere that event happened. Then what happens is it starts to bubble up. Meaning it will trigger a click on all the things previously mentioned. We can sort of stop it with capture
.
There is an option so that you can listen during the capture phase instead of during the bubbling phase. This is done with a 3rd argument.
Syntax for addEventListener();
target.addEventListener(type, listener[, options]);
/Sauce
target.addEventListener(type, listener[, useCapture]);
We will be passing an options object. What we can do is edit our window event listener:
const buyButtons = document.querySelectorAll('button.buy'); function attachListener (event) { //remember event can be called anything console.log('You clicked a button'); //** CHANGED***** console.log(paseFloat(event.target.dataset.price)); console.log(event.target); console.log(event.currentTarget); console.log(event.target === event.currentTarget); //event.stopPropagation(); }; buyButtons.forEach(function(buyButton){ buyButton.addEventListener('click', attachListener); //1st argument window.addEventListener('click', //2nd argument () => { console.log('YOU CLICKED THE WINDOW'); }, //3rd argument //{capture:true}; //************ ABOVE IS THE CAPTURE (A 3RD ARGUMENT) *************
I commented out the
event.stopPropagation()
and thecapture:true
and edited the 1st console.log output to "You clicked a button"
The console log will show the button
first, then window
. However, if you uncomment out the capture:true
line the order of the console log is reversed. You will get window
first then button
. Meaning the order of the capture and bubble action happens in reverse order.
const buyButtons = document.querySelectorAll('button.buy'); function attachListener (event) { //remember event can be called anything console.log('You clicked a button'); //** CHANGED***** console.log(paseFloat(event.target.dataset.price)); console.log(event.target); console.log(event.currentTarget); console.log(event.target === event.currentTarget); //event.stopPropagation(); }; buyButtons.forEach(function(buyButton){ buyButton.addEventListener('click', attachListener); //1st argument window.addEventListener('click', //2nd argument () => { console.log('YOU CLICKED THE WINDOW'); console.log(event.target); event.stopPropagation(); }, //3rd argument //{capture:true}; //************ ABOVE IS THE CAPTURE (A 3RD ARGUMENT) *************
That change in the 2nd argument above while keeping the 3rd argument will stop the propagation down instead of bubbling up - this is capture
. When you run this and click the button, it will show "YOU CLICKED THE WINDOW" in the console and the attachListener
won't get to run.
This is all probably good to know, but I've probably used that three times in my entire career. That may be more of an interview question. Most of my career has been spent listening to events on lower level elements and stopping the event from propagating.
Wes Bos
Events - Prevent Default and Form Events
Prevent Default
There are a couple of elements in HTML that have default functionality when they are used. Forms and anchor tags are a cheap example, think about what happens when you click "submit" on a typical form made in pure HTML it will act like most event handlers in Javascript.
Let's look at an example on some HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>HTML Forms</title> <link rel="stylesheet" href="../../base.css"> </head> <body> <div class="wrapper"> <a class="wes" href="https://wesbos.com">Wes Bos</a> <form name="signup"> <label for="name">Name</label> <input type="text" id="name" name="name" value="Wes Bos"> <label for="email">Email</label> <input type="email" id="email" name="email" value="[email protected]"> <input type="checkbox" id="agree" name="agree" required> <label for="agree">I agree to the terms and conditions</label> <hr> <button type="submit">Submit</button> </form> </div> <script src="./forms.js"></script> </body> </html>
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ event.preventDefault(); }; });
In this example, the anchor
tag on line 13 above won't work when you click it. That's cool and all but what can you do with it?
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ event.preventDefault(); //********** NEW STUFF BELOW THIS *********** const shouldChangePage = confirm( `This website might be educational! Do you wish to Learn?` ); if (shouldChangePage) { //if it's truthy! window.location = event.currentTarget.href; } console.log(shouldChangePage); }; });
If you run the above, you force a prompt that says "This website might be educational! Do you wish to Learn?" - if you confirm it (click 'ok'), then a console log will show the confirmation and redirect you to https://wesbos.com (if you are wondering where that came from like I was - it's in line 13 of the HTML above). We can also simplify that with something more like this:
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ const shouldChangePage = confirm( `This website might be educational! Do you wish to Learn?` ); if (!shouldChangePage) { event.preventDefault(); // **** CHANGES MADE HERE }; });
Preventing default is extremely handy when trying to prevent the default event from happening. So what other defaults are there on a page? Submitting a form is a good one.
If you'd like to select a form by it's
name
in Javascript you'd use the attributes selector. You will see it in action below:
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ const shouldChangePage = confirm( `This website might be educational! Do you wish to Learn?` ); if (!shouldChangePage) { event.preventDefault(); // **** CHANGES MADE HERE }; }); // ************* NEW STUFF BELOW HERE ************************* const signupForm = document.querySelector('[name="signup"]'); singupForm.addEventListener('submit', function(event){ console.log(event); event.preventDefault(); console.dir(event.currentTarget); } );
Now if you click the Submit button - This will actually prevent the form from submitting and allow us to use Javascript to pull the data from those forms and add them to the database.
Forms (preventing default events)
If you look in the console log and expand the form, you will see name
in the tree and all the form's entries. So you can see that there is the ability to pull that information. Let's dive in to how that works.
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ const shouldChangePage = confirm( `This website might be educational! Do you wish to Learn?` ); if (!shouldChangePage) { event.preventDefault(); // **** CHANGES MADE HERE }; }); const signupForm = document.querySelector('[name="signup"]'); singupForm.addEventListener('submit', function(event){ console.log(event); event.preventDefault(); // ************* NEW STUFF BELOW HERE ************************* console.log(event.currentTarget.name); console.log(event.currentTarget.name.value); console.log(event.currentTarget.email); console.log(event.currentTarget.email.value); console.log(event.currentTarget.agree); console.log(event.currentTarget.agree.checked); } );
In the console, you will see "Form Name", "Wes Bos" (The name of the form), "Email", "[email protected]" (the value of the email field), and "true" because the box is checked (and required).
(Sorry in advanced if your name is Chad - Now you can select the individual field inputs like this:
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ const shouldChangePage = confirm( `This website might be educational! Do you wish to Learn?` ); if (!shouldChangePage) { event.preventDefault(); // **** CHANGES MADE HERE }; }); const signupForm = document.querySelector('[name="signup"]'); singupForm.addEventListener('submit', function(event){ event.preventDefault(); // ************* NEW STUFF BELOW HERE ************************* const name = event.currentTarget.name.value; if(name.includes('chad')){ alert('Sorry bro'); event.preventDefault(); } );
In the example above, if you ran it and you tried to input a name with "chad" in it, you'd get a warning saying "Sorry Bro"! Otherwise, submissions on the form will act normally. That's pretty cool and is a pretty powerful toolset we just unlocked.
A bit of an Aside:keyup
& keydown
Note: In the previous example - This guide isn't going into regular expressions (regex) which you can use in includes() to search for "Chad" as well as "chad" since this is case sensitive. So for now, this form will just prevent "chad" but not "Chad".
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ const shouldChangePage = confirm( `This website might be educational! Do you wish to Learn?` ); if (!shouldChangePage) { event.preventDefault(); // **** CHANGES MADE HERE }; }); const signupForm = document.querySelector('[name="signup"]'); singupForm.addEventListener('submit', function(event){ event.preventDefault(); const name = event.currentTarget.name.value; if(name.includes('chad')){ alert('Sorry bro'); event.preventDefault(); } ); // ************* NEW STUFF BELOW HERE ************************* //GETTING CREATIVE WITH AN EXTERNAL FUNCTION TO CALL IN THE EVENT ๐ function logEvent(event){ console.log(event.type); console.log(evenet.currentTarget.value); }; //* THIS IS A KEYUPEVENT: //EVENT LISTENER *********** signupForm.name.addEventListener( //1ST ARGUMENT 'keyup', //2ND ARGUMENT logEvent; ); //*********** THIS IS A KEYDOWN EVENT:***************** //EVENT LISTENER signupForm.name.addEventListener( //1ST ARGUMENT 'keydown', //2ND ARGUMENT logEvent; );
You can use
event.key
or (less supported)event.keycode
to listen for a specific key press if you were say looking for anโฎ
press. Wes created a page called keycode.info that will allow you to press any key on the keyboard and see the meta information about that key.
This new addition will give us both the name of the event keyup
& keydown
and the information the user has typed into the field! Pretty neat! So if you don't want certain letters or values from being input, you'd want to use a keydown
.
Aside: focus
& blur
focus
and blur
can both be accomplished with CSS - this will show you how to do it with events to focus on a field when clicked or blur and object when clicked using Javascript.
const wes = document.querySelector('.wes'); wes.addEventListener( //1st argument 'click', //2nd argument function(event){ const shouldChangePage = confirm( `This website might be educational! Do you wish to Learn?` ); if (!shouldChangePage) { event.preventDefault(); // **** CHANGES MADE HERE }; }); const signupForm = document.querySelector('[name="signup"]'); singupForm.addEventListener('submit', function(event){ event.preventDefault(); const name = event.currentTarget.name.value; if(name.includes('chad')){ alert('Sorry bro'); event.preventDefault(); } ); //GETTING CREATIVE WITH AN EXTERNAL FUNCTION TO CALL IN THE EVENT ๐ function logEvent(event){ console.log(event.type); console.log(evenet.currentTarget.value); }; signupForm.name.addEventListener( 'keyup', logEvent; ); signupForm.name.addEventListener( 'keydown', logEvent; ); // ************* NEW STUFF BELOW HERE ************************* signup.name.addEventListener('focus', logEvent); signup.name.addEventListener('blur', logEvent);
Events - Accessibility Gotchas & Keyboard Codes
Accessibility is an important part of web development today - these are some things to keep in mind, mostly pitfalls.
Links vs Buttons
Links should be used to change the URL, buttons should be used to make changes within an application or page. Don't mix these two up!
Keyboard Accessibility
Things That Are Not Keyboard Accessible Should not have clicks registered on them unless you need to. There for example, lots of valid use cases to click on a photo, but if you through your mouse away - you cannot click on the photo without a keyboard. You can resolve this by adding an attribute to an image like this: role="button"
and tabindex="0"
. This will allow you to tab through and select the image. You may also need to create an event listener to allow a keyup
event as well as the click
.
Another way to do this:
function handlePhotoClick(event) { if(event.type === 'click' || event.key === "Enter"){ console.log('You clicked the photo'); console.log(event.key); } };
The above snippet will allow you to click or press the return key to get the click event to kick off.
My Ask & Final Thoughts
Event listeners are a super powerful tool to allow users to interact with your website and builds upon the DOM posts I shared recently. The possibilities of what you can do with Javascript after writing this just has my head going with cool things I could accomplish - it also makes some of Wes' javascript30.com's exercises make a whole heck of a lot more sense! If you haven't checked those out, I'd definitely recommend it!
If you found this article helpful, share/retweet it and follow me on twitter @codingwithdrewk! There is so much more in Wes' courses I think you will find valuable as I have. I'm learning so much and really enjoying the course, Wes has an amazingly simple way to explain the difficult bits of Javascript that other courses I've taken could only wish. You can view the course over at WesBos.com. (I am in no way getting any referrals or kickbacks for recommending this)
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.