In this lesson, we'll look at a solution of a typical frontend problem with what we learned earlier. Imagine a regular menu, and whenever the user clicks on something, an active element changes.
See the Pen js_dom_events_in_action_nav by Hexlet (@hexlet) on CodePen.
The principle of how it works here is as follows. The link that the user clicks on is given the active
class, which makes a new menu item stand out. With the previous selected item, exactly the opposite happens, in that case, 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. This 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? The problem is that we don't know which element was selected. There are several solutions to this problem.
Memorize the selected element and remove the desired class from it when the new element is clicked. This method requires a state to be introduced; some kind of variable that stores the currently selected element. Adding additional complexity is the first load, 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. In general, this method is quite demanding to implement.
There's a much easier method. Instead of trying to update the elements point-by-point, we'll just deselect all the elements at once. And it doesn't matter whether the element has already selected or not – the class deletion operation is idempotent, i.e., it doesn't lead to an error if the element didn't have a class to be deleted.
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 does do a little more work. That would be it, but there's one issue. Menu initialization doesn't work properly. Why?
Not every menu is controlled by js, but the current code doesn't take this into account. It'll try to work with every menu on the page, which isn't what we want. The proper way to do it is based on using a special pointer, which lets us know whether the menu should be controlled using js or not. It's considered good practice to use data-* attributes for this.
I.e., adding js 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. Now it assumes that each page has only one page. But the nav component can be added to a page any number of times. For example, there are places on Hexlet with three menus on one page.
If you run the current code with several menus, you may notice the following bug. Clicking on any of the items on one menu removes activity from the other menu. It happens because of this line:
// links refers 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 and which ones don't? To do this, you need to perform two actions:
links.forEach((link) => {
link.addEventListener('click', () => {
// 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');
});
});
The Hexlet support team or other students will answer you.
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.
Programming courses for beginners and experienced developers. Start training for free
Our graduates work in companies:
From a novice to a developer. Get a job or your money back!
Sign up or sign in
Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.