Passing Arguments to Event Handlers
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
.
/**
* 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
bind
method returns a new function.this
keyword in the linkHoverHandler
function is now set to the value we passed into the bind
method (0.5 or 1).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.addEventListener
method. It will always have one real parameter, which is the event object.