JavaScript JavaScript and the DOM Responding to User Interaction Listening for Events with addEventListener()

Cameron Childres
MOD
Cameron Childres
Treehouse Moderator 11,666 Points

I left out "let" when initializing a loop and spent ages debugging. Can someone help me understand scope here?

Here was my initial attempt at styling list items on mouseover:

const listItems = document.getElementsByTagName('li');

for (i=0; i<listItems.length; i++) {
  listItems[i].addEventListener ('mouseover', () => {
    listItems[i].style.textTransform = "capitalize";
  });
  listItems[i].addEventListener ('mouseout', () => {
    listItems[i].style.textTransform = "lowercase";
  });
}

This adds the event as I expected but when it tries to set the style the console gives the error Uncaught TypeError: Cannot read property 'style' of undefined at HTMLLIElement.. It took me far too long to realize I didn't start off with "let i=0" which makes things behave as intended, and which I now know has to do with block scope.

After some digging I'm still left scratching my head about a couple of things:

  • Why does the event still get added without "let" ever being there?
  • Why does using "var" instead of "let" give the same error as omitting it? Shouldn't this all fall under the function scope of var or does that not apply to loops?

And a bonus question:

  • My first instinct was to style with CSS, the video transforms the text purely with JS. Is there any benefit to setting styles like this with CSS vs JS?

Workspace link. Excuse the notes from me working through this :)

1 Answer

Dane Parchment
MOD
Dane Parchment
Treehouse Moderator 10,232 Points

So what's happening isn't exactly a scope issue, it's a misunderstanding of how event listeners and loop counters work.

Basically what you are doing is creating a loop that applies an event listener to all the li elements on a page. The way you've done it is fine. However, the problem is how you are implementing what you are doing inside the event listener.

So How Do They Work?

An event listener works by adding a function to a list of eventlisteners for that specific event you are targeting. This is important because basically that means that events are called after the code that created the event listener is run. This means that how you define the code inside your event listeners, is important.

With loop counters (for loop I should specify) the counter continues until the condition that satisfies the for loop is met. In this case the loop will continue until the loop counter is greater than the length of items in the list remember that.

So What's Broken?

Two thing are wrong with the code that you wrote both are affected by the factors I mentioned above.

  1. The loop counter is greater than the number of items in the list, this is because the loop counter continued until the condition was satisfied, namely: once the counter was greater than the number of list items.
  2. The code inside the event listener is referencing this counter which means you'll get undefined because: array[length + 1] does not exist.

What You Thought Was Happening

So let me explain what I believe you though you were doing with the event listener code. You were thinking that when you created the code to capitalize and lowercase the text: listItems[i].style.textTransform = "capitalize"; that [i] being used would be the one that was being looped at the time. This is what you thought was happening inside each event listener:

listItems[0].style.textTransform = "capitalize"; // For loop 1, Event Listener 1
listItems[1].style.textTransform = "capitalize"; // For loop 2, Event Listener 2
listItems[2].style.textTransform = "capitalize"; // For loop 3, Event Listener 3
listItems[3].style.textTransform = "capitalize"; // For loop 4, Event Listener 4

What Is Really Happening

But what is actually happening is that the code inside of the event listener, is just a function. It isn't compiled, it's provided to a listener to run when that event fires. So what does the event listener actually see inside each event? What you literally provided it

listItems[i].style.textTransform = "capitalize"; // For loop 1, Event Listener 1
listItems[i].style.textTransform = "capitalize"; // For loop 2, Event Listener 2
listItems[i].style.textTransform = "capitalize"; // For loop 3, Event Listener 3
listItems[i].style.textTransform = "capitalize"; // For loop 4, Event Listener 4

So when the event fires, the function that runs is: listItems[i].style.textTransform = "capitalize"; and so the javascript interpreter will look for the i variable in it's scope and find the value 4. It will then try to apply the text transformation to it, and find that it doesn't exist because index 4 doesn't exist in the array! Understand now?

So How Do We Fix This?

Very easily actually, all we need to do is make the function inside the event listener more accommodating for the dynamic nature of an event. So instead of targeting a specific array item, we'll use the event object to grab the html element (the target of the event) and apply the transformation to that instead.

for (var i=0; i<listItems.length; i++) {
  console.log(`In Loop: ${i}`);
  listItems[i].addEventListener ('mouseover', (event) => {
    event.target.style.textTransform = "uppercase";
  });
  listItems[i].addEventListener ('mouseout', (event) => {
    event.target.style.textTransform = "lowercase";
  });
}

Hopefully that helps! I know this was probably a confusing read, but if you need more clarification feel free to ask.

Cameron Childres
Cameron Childres
Treehouse Moderator 11,666 Points

Thank you for the thorough response! You're spot on with what I thought was happening, and I'm kicking myself for not thinking to log i in the console to troubleshoot -- very useful trick. I had gotten it to work with event.target but didn't understand the why so this is very helpful.

Can you expand on why using let i=0 gives the proper values of [i]? Does this mean that when the function is initially passed to the event listener it's giving the current value of [i] in the loop as opposed to the variable itself? I'm a bit fuzzy on why let seems to "lock in" the variable at the loop value. For an example here's Guil's code from the video:

/* Guil's code copied from video, works with 'listItems[i]' in event handler*/

for (let i = 0; i< listItems.length; i +=1) {
  listItems[i].addEventListener('mouseover', () => {
    listItems[i].textContent = listItems[i].textContent.toUpperCase();
  });
  listItems[i].addEventListener('mouseout', () => {
    listItems[i].textContent = listItems[i].textContent.toLowerCase();
  });
}
Dane Parchment
Dane Parchment
Treehouse Moderator 10,232 Points

No problem! Glad to be of help. Sorry for not responding immediately I just got back from work. So let's (haha) jump into this!

let works because it is a scoped variable that is scoped to the block-level whereas the var variable is scoped to the global-level.

What Does This Mean?

Global level should be self explanatory, it means that the code is reference-able from anywhere in that file. It basically leaked into window (or parent) scope through the use of hoisting.

Block level however, means that the variable is locked to a block. What is a block? Anything in a curly brace. So basically any code in here:

{
// Code found between these curly braces are in the block-level
}

As an example to see this in action (Do this in your workspaces don't do it in the developer console as the developer console is all scoped):

var i = 10;
if(i == 10) {
    var i = 18;
    console.log(i);
}
console.log(i);

When you run this, you'll get 18 for both console logs. Why? Because of hoisting var scoped variables are locked to the global scope, so the interpreter actually sees this:

var i;
i = 10;
if(i == 10) {
    i = 18;
    console.log(i);
}
console.log(i);

This is why when you use code that doesn't run at the same time as other code (promises, timeouts, events), it uses the end result of that scoped variable because the variable has already been set.

So how does let change this? Well let's see, change the code in the previous post to be let instead:

let i = 10;

if(i == 10) {
    let i = 18;
    console.log(i);
}
console.log(i);

When you run this, you'll get: 18, and then respectively 10. What we basically expect, because the code inside the curly braces of that if statement, are it's own thing, the interpreter would basically (not exactly, but you'll get the point) see it like this:

var i;
i = 10;
if(i == 10) {
    var i_2;
    i_2 = 18;
    console.log(i_2);
}
console.log(i);

As you can see the let variable inside the if statement is treated like it's own variable as opposed to the let outside of the if statement.

So why does let work for the loop? Well let's give an example using var and an example using let on a simplified version of your code, and demonstrating the result of what it looks like when the event is fired:

for (i=0; i<2; i++) { // Equivalent to: var i = 0; i < 2; i++
  listItems[i].addEventListener ('mouseover', () => {
    listItems[i].style.textTransform = "capitalize";
  });
  listItems[i].addEventListener ('mouseout', () => {
    listItems[i].style.textTransform = "lowercase";
  });
}

When interpreted looks like this:

var i;
for(i = 0; i < 2; i++) { //first iteration
   i = 1; // second iteration
   i = 2; // end result loop satisfied
   // Loop code goes here .....
}
// When the event is fired:
// it will be the same regardless of the item moused over because they are all referring to i
listItems[i].style.textTransform = "capitalize"; 

See how this will cause problems? Now let's look at let:

for (let i=0; i<2; i++) {
  listItems[i].addEventListener ('mouseover', () => {
    listItems[i].style.textTransform = "capitalize";
  });
  listItems[i].addEventListener ('mouseout', () => {
    listItems[i].style.textTransform = "lowercase";
  });
}

When interpreted looks like this:

for(var i; i < 2; i++) {
    var i;
    var i_2;
    var i_3;
    i = 0; // first iteration
    i_2 = 1; // second iteration
    i_3 = 2; // end result loop satisfied (don't worry about how it determines this for now, you can research it if you want)
}
// When event is fired:
listItems[i].style.textTransform = "capitalize";
// When the second list item is fired
listItems[i_2].style.textTransform = "capitalize";

Now do you see how it would work and why it works for let vs var? (It won't work for const because const cannot be overwritten which is necessary for a loop)

Hope that helps!

Cameron Childres
Cameron Childres
Treehouse Moderator 11,666 Points

You're a legend Dane! I really appreciate you taking the time to help and to write all of it out -- you've cleared up SO much of my confusion here :)

Dane Parchment
Dane Parchment
Treehouse Moderator 10,232 Points

No problem man, glad to be of help. Just keep doing what you're doing, you're asking the right questions and thinking critically, that's the makings of a successful programmer.