In this lesson, we'll look at a solution to a typical front-end task with what we learned earlier.
Let's imagine a typical menu. An active element changes when a user clicks on something:
See the Pen js_dom_events_in_action_nav by Hexlet (@hexlet) on CodePen.
Let's observe how it works. A user clicks on the link, so the program adds the active
class to the given link. It makes a new menu item stand out.
The opposite happens with the previously selected item. The link loses the active
class.
If we're going to do it straightforwardly, we need to extract all the menu links from the DOM and assign a click handler to each of them.
The handler will activate the current element and deactivate the previous one:
// We need to extract all links
const links = document.querySelectorAll('a');
// Each button has an event
// To do this, we bypass all the links and assign a handler to each of them
links.forEach((link) => {
link.addEventListener('click', () => {
// We need to deactivate the previous selected item
// Select the current one
link.classList.add('active');
});
});
How do we remove the highlighting from the previous element correctly? We don't know which element was selected, so there are several solutions to this problem.
Solution 1. To memorize the selected element and remove the desired class when users click a new element.
To use this method, we introduce a state — a variable that stores the currently selected element. The first load adds extra complexity since the highlighted element most likely already comes in finished HTML, which means there is no information in the js yet about who is active. This method is quite demanding to implement.
Solution 2. To deselect all the elements at once. There's a much easier method. Instead of trying to update the elements one by one, we'll just deselect all the elements at once. It doesn't matter whether the element has already been selected or not.
The class deletion operation is idempotent. In other words, it doesn't lead to an error if the element doesn't have a class to delete:
links.forEach((link) => {
link.addEventListener('click', () => {
// Removing the active class from all links
links.forEach((link) => link.classList.remove('active'));
link.classList.add('active');
});
});
This solution is much simpler, although it demands a little more work. It would be it, but there's one issue. Menu initialization doesn't work correctly. Why?
We can't control every menu with JavaScript, but the current code doesn't take it into account. The code will try to work with every menu on the page, which isn't what we want.
The proper way to do it is to use a pointer. It will let us know whether the menu should be controlled with JavaScript or not. A good practice is to use data-*
attributes for this.
So, adding JavaScript logic only works for those menus that have the desired attribute:
<ul class="nav">
<li>
<a class="active" data-toggle="tab" href="#home">Home</a>
</li>
<li>
<a class="active" data-toggle="tab" href="#profile">Profile</a>
</li>
<li>
<a class="active" data-toggle="tab" href="#contact">Contact</a>
</li>
</ul>
Accordingly, our code should change to look like this:
const links = document.querySelectorAll('[data-toggle="tab"]');
But even in this case, the code doesn't work properly. It assumes that each page has only one page. But we can add the nav component to a page any number of times. For example, somewhere on Hexlet, we have three menus on one page.
If you run the current code with several menus, you may notice a bug: clicking on any of the items on one menu removes activity from the other menu.
It happens because of this line:
// The links refer to all links in all menus on the page
links.forEach((link) => link.classList.remove('active'));
To solve this problem, you need to update only those links that relate to the current menu. But how do you know which ones relate to it?
To do this, you need to perform two actions:
- Find the current menu's root element, which was clicked
- Find the active link inside the current menu and remove the active class
links.forEach((link) => {
link.addEventListener('click', () => {
// The `closest` finds the nearest parent with the required selector
// Our menu has the .nav class
const nav = link.closest('.nav');
// Find the active element inside the menu
const activeElement = nav.querySelector('.active');
activeElement.classList.remove('active');
link.classList.add('active');
});
});
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.