const - JS | lectureAdvanced DOM and Events

Passing Arguments to Event Handlers

Advanced DOM and Events

In this lecture, we will create a nice effect on the navigation bar we had in the lecture on Event Propagation in Practice. We will make all the links fade out when we hover over one of them, except the one we are hovering over. And this will teach us something very valuable, which is how to pass arguments to event handler functions. So if you haven't yet downloaded the starter files, head over to the event propagation practice lecture, download the files, and let's get started.

As we've learned in the previous lectures, we do not want to attach an event listener to each of our navigation links. Instead, we will once more use event delegation. And as always with event delegation, the first step is to find the common parent element of all the elements we are interested in. In this case, that is the ul element with the class nav__links. So let's select that element and store it in a variable called navLinks.

script.js
/**
 * Keep in mind that all this works because events bubble up
 * from their target!
 */
const navLinks = document.querySelector('.nav__links');

Let's now attach an event listener to this element. And this time, we are not going to use the click event. Instead, we will use the mouseover event. The mouseover event is a bit similar to the mouseenter event, with the big difference that mouseenter does not bubble up.

There are also opposite events to mouseover and mouseenter which we use to basically undo what we do on the hover. So the opposite of mouseover is mouseout and the opposite of mouseenter is mouseleave. So let's also attach an event listener to the navLinks element for the mouseout event.

script.js
navLinks.addEventListener('mouseover', function (e) {
  // Do something
});

navLinks.addEventListener('mouseout', function (e) {
  // Do something
});

As always if you need to know more about the different events, then you can check out the MDN documentation.

The next thing we need to do now is to match the elements that we are interested in. In our case, we want to match all the links that are inside the navLinks element with the class nav__link. Let's do that.

script.js
navLinks.addEventListener('mouseover', function (e) {
  if (e.target.classList.contains('nav__link')) {
    const hoveredLink = e.target;
  }
});

You can see that now we haven't used the closest method. And that's because, in this case, we do not have a child element in our links that we could accidentally click as we had in the previous lecture when building the tabbed component.

Next, we now need to select the sibling elements of the hovered link. Remember that we can do that by going to the parent and then selecting all the children. In our case, the parent of nav__link is actually nav__item. And the only thing that nav__item has is always just one link. So now we would have to move up manually, not only once but twice. Hence, instead of doing that, we will again use the closest method.

script.js
navLinks.addEventListener('mouseover', function (e) {
  if (e.target.classList.contains('nav__link')) {
    const hoveredLink = e.target;

    const siblings = hoveredLink      .closest('.nav__links')      .querySelectorAll('.nav__link');  }
});

Now that we have all our elements selected, we now have to change the opacity of the siblings of the selected link.

script.js
navLinks.addEventListener('mouseover', function (e) {
  if (e.target.classList.contains('nav__link')) {
    const hoveredLink = e.target;

    const siblings = hoveredLink
      .closest('.nav__links')
      .querySelectorAll('.nav__link');

    siblings.forEach((sibling) => {      if (sibling !== hoveredLink) {        sibling.style.opacity = 0.5;      }    });  }
});

Right now, we have the desired effect, but we now need to make it go back automatically to an opacity of 1 when we move out. And that's why we added the event listener for the mouseout event. Let's just copy the code from the mouseover event and change the opacity back to 1.

script.js
navLinks.addEventListener('mouseout', function (e) {
  if (e.target.classList.contains('nav__link')) {
    const hoveredLink = e.target;

    const siblings = hoveredLink
      .closest('.nav__links')
      .querySelectorAll('.nav__link');

    siblings.forEach((sibling) => {
      if (sibling !== hoveredLink) {
        sibling.style.opacity = 1;      }
    });
  }
});

Everything now works as expected, but we now have a problem. Our solution is very repetitive. The code we've written is always the same. Hence we need to make our code more DRY. So let's refactor our code. And usually refactoring works by creating a new function.

script.js
const linkHoverHandler = function () {
  // Do something
};

We now need to compare the code we've written for the mouseover event and the mouseout event, and then compare what is the same and what is different. So, we can see that the only thing that is different is the opacity. Hence, we can remove the code from both handlers and then create a parameter for the opacity. Which then can be passed to the function.

script.js
const linkHoverHandler = function (e, opacity) {  if (e.target.classList.contains('nav__link')) {
    const hoveredLink = e.target;

    const siblings = hoveredLink
      .closest('.nav__links')
      .querySelectorAll('.nav__link');

    siblings.forEach((sibling) => {
      if (sibling !== hoveredLink) {
        sibling.style.opacity = opacity;      }
    });
  }
};

navLinks.addEventListener('mouseover', function (e) {
  // Do something
});

navLinks.addEventListener('mouseout', function (e) {
  // Do something
});

Now how do we use our new function? Well, usually, when we have our event handler as a separate function, all we do is to pass in that function and it's going to work.

script.js
navLinks.addEventListener('mouseover', linkHoverHandler); // Will not work
navLinks.addEventListener('mouseout', linkHoverHandler); // Will not work

But the problem now is that we actually want to pass in arguments into our function. Maybe you taught we could do something like this.

script.js
navLinks.addEventListener('mouseover', linkHoverHandler(e, 0.5)); // e is not defined
navLinks.addEventListener('mouseout', linkHoverHandler(e, 1)); // e is not defined

But this won't also work because first, you will get an error saying e is not defined, and secondly, addEventListener expects a function. So we need to pass in a function. But if we call a function, then the second argument will be some other value. And in our case, it is undefined since our function returns nothing.

The solution to this problem would be to have a regular callback function still, but inside that callback function, we would call our function and pass in the arguments.

script.js
navLinks.addEventListener('mouseover', function (e) {
  linkHoverHandler(e, 0.5);});

navLinks.addEventListener('mouseout', function (e) {
  linkHoverHandler(e, 1);});

With this, everything is back to now. But we can actually even do better using the bind method. Remember that the bind method creates a copy of the function it is called on, and it will set the this keyword to whatever value we pass into it.

script.js
const linkHoverHandler = function (e) {  if (e.target.classList.contains('nav__link')) {
    const hoveredLink = e.target;

    const siblings = hoveredLink
      .closest('.nav__links')
      .querySelectorAll('.nav__link');

    siblings.forEach((sibling) => {
      if (sibling !== hoveredLink) {
        sibling.style.opacity = this;      }
    });
  }
};

navLinks.addEventListener('mouseover', linkHoverHandler.bind(0.5));navLinks.addEventListener('mouseout', linkHoverHandler.bind(1));

Note the following:

  1. The bind method returns a new function.
  2. The this keyword in the linkHoverHandler function is now set to the value we passed into the bind method (0.5 or 1).
  3. By default, the this keyword is equal to e.currentTarget, which is the element that the event handler is attached to. But when we then set the this keyword manually, it will be set to whatever value we pass into the bind method.
  4. Essentially, we use the bind method to pass arguments into the event handler function, either as an object or an array of values.
  5. It is impossible to pass an argument into an event handler function when using the addEventListener method. It will always have one real parameter, which is the event object.