jeromebg
jeromebg

Reputation: 19

How to efficiently / correctly handle the click-event at any list-item of a multiple nested list-based menu / navigation?

Sorry if my question is really for newbie level...

I have a 3 level list to build a menu, but I don't want to use href. I would like to use a specific attribute instead and call a javascript function with it value.

Here is the markup of my example-list:

<ul class="menu">
<li><a href="#" link_value="item 1">Firts level Menu item 1</a></li>
    <ul>
        <li><a href="#" link_value="item 2-1">Second level Menu item 1</a></li>
            <ul>
                <li><a href="#" link_value="item 3-2-1">Third level Menu item 1</a></li>
                <li><a href="#" link_value="item 3-2-2">Third level Menu item 2</a></li>
                <li><a href="#" link_value="item 3-2-3">Third level Menu item 3</a></li>
            </ul>
        <li><a href="#" link_value="item 2-2">Second level Menu item 2</a></li>
        <li><a href="#" link_value="item 2-3">Second level Menu item 3</a></li>
    </ul>
<li><a href="#" link_value="item 2">Firts level Menu item 2</a></li>
<li><a href="#" link_value="item 3">Firts level Menu item 3</a></li>
How could I run a javascript function to set onclick event for all a items to have something like
<a onclick="my_fynction()">
function my_fonction() {do_something(link_value)}

and also add an 'active' class to the active element and his parent levels and remove the active class from the others ?

Hope I have been clear enough ? Many thanks !!!

Upvotes: -1

Views: 157

Answers (2)

Peter Seliger
Peter Seliger

Reputation: 13432

Regarding both of my above comments ...

@jeromebg ... link_value is an invalid attribute name. Please consider making use of a data-* global attribute and the DOM element's related dataset property instead ... something like e.g ... <a href="#" data-value="item 3"/> where each value then can be read like this ... linkElement.dataset.value.

@jeromebg ... the nested <ul/> markup is broken/invalid as well ... please fix it.

... and after having fixed the markup, one should make use of, as already suggested but not explicitly named, event-delegation.

The latter does not only mean listening at a common (outer) root node, it also means targeting the element/s of interest which, like the root-node, might have child element-nodes as well.

Thus one always has to query the element/s one is interested in. One mostly does achieve the result by utilizing the closest method of the event's target element.

And regarding another of the OP's requirements ...

... also add an 'active' class to the active element and his parent levels and remove the active class from the others ?

... the handler function would pass the currently identified link-element to a custom implemented function, thus forwarding such a special task and not taking care of such stuff by itself.

A function which marks the current menu item has to do following ...

  • identifying the menu-item node ... done by ...

    const menuItem = elmLink.closest('li');
    
  • identifying the menu-root node ... done by ...

    const menuRoot = menuItem.closest('ul.menu');
    
  • choosing the right selector for querying any list-item node which has a link-element (e.g. 'li:has(a[href])') and removing the intended class-name (and/or e.g. aria attributes) from each queried element.

  • adding the intended class-name (and/or e.g. aria attributes) to the list-item which has been identified as current.

It is not necessary to mark any list-item involved in another list-item's current state as current as well. The visual representation of such a state can be easily achieved by utilizing a functional CSS pseudo-class like :has()

function markCurrentMenuItem(elmLink) {
  const menuItem = elmLink.closest('li');
  const menuRoot = menuItem.closest('ul.menu');

  menuRoot
    .querySelectorAll('li:has(a[href])')
    .forEach(elm => {

      // elm.classList.remove('active');
      elm.removeAttribute('aria-current');
    });
  // menuItem.classList.add('active');
  menuItem.setAttribute('aria-current', 'true');
}
document
  .querySelector('ul.menu')
  .addEventListener('click', evt => {
    const elmLink = evt.target.closest('a[href][data-value]')

    if (elmLink) {
      evt.preventDefault();

      markCurrentMenuItem(elmLink);

      console.log({ value: elmLink.dataset.value });
    }
  })
body { margin: 0; }
ul { list-style: none; margin: 0; padding: 0 0 0 20px; }
li { padding: 2px 0 2px 0; }

a span { display: inline-block; padding: 0 20px; background-color: #f2ffa7; }

/* li.active, */li[aria-current="true"] a > span {
  background-color: #b1d000;
}
/* ul:has(> li.active), */ul:has(> li[aria-current="true"]) {
  background-color: #bee8ff;
}
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<ul class="menu" role="menu" aria-label="Main Menu">
  <li>
    <a href="#" data-value="item 1">
      <span>First level Menu item 1</span>
    </a>
  </li>
  <li>
    <ul role="menu" aria-label="Second Level Menu">
      <li>
        <a href="#" data-value="item 2-1">
          <span>Second level Menu item 1</span>
        </a>
      </li>
      <li>
        <ul role="menu" aria-label="Third Level Menu">
          <li>
            <a href="#" data-value="item 3-2-1">
              <span>Third level Menu item 1</span>
            </a>
          </li>
          <li>
            <a href="#" data-value="item 3-2-2">
              <span>Third level Menu item 2</span>
            </a>
          </li>
          <li>
            <a href="#" data-value="item 3-2-3">
              <span>Third level Menu item 3</span>
            </a>
          </li>
        </ul>
      </li>
      <li>
        <a href="#" data-value="item 2-2">
          <span>Second level Menu item 2</span>
        </a>
      </li>
      <li>
        <a href="#" data-value="item 2-3">
          <span>Second level Menu item 3</span>
        </a>
      </li>
    </ul>
  </li>
  <li>
    <a href="#" data-value="item 2">
      <span>First level Menu item 2</span>
    </a>
  </li>
  <li>
    <a href="#" data-value="item 3">
      <span>First level Menu item 3</span>
    </a>
  </li>
</ul>

Upvotes: 0

digitalniweb
digitalniweb

Reputation: 1142

You can listen to clicks on ul.menu and put your event logic only on this element.

document.querySelector('ul.menu').addEventListener('click', event => {
    // event.target is clicked element
    if(event.target.tagName!=="A") return;
    // prevent default behaviour; redirecting
    event.preventDefault();
    // your code...
})

But you'd need to be careful if you put <span> for example inside <a>. You'd need to put CSS pointer-events: none; on these elements.

Upvotes: 2

Related Questions