Working with dropdowns


#1

This is my fiddle My Work
I want to change the hover to onClick. A little vanilla js


#2

Your current CSS implementation is good to have for when scripts are not enabled. There is nothing to change there, but the script will need to override the hover CSS event.

We could start by caching the parent element, navbar and then add an event listener to that. Any mouse (or focus) events that occur within the scope of that node will bubble up to the parent, at which time we can delegate which handler to fire.

I’ll let you investigate this a little further while I play around with it.


#3

Update

Have some working code for you to try. It’s not perfect, but it does work…

const navBar = document.querySelector('.navbar'); 

const dropMenu = document.querySelectorAll('.dropdown-content');

//https://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/
const getEventTarget = e => {
  e = e || window.event;
  return e.target || e.srcElement;
};

const noHover = () => {
  dropMenu.forEach(el => {el.style.display = "none";});
}

const dropDown = (e) => {
  let target = getEventTarget(e);
  if (target.className === 'dropbtn') {
    target.nextElementSibling.style.display = 'block';
  }
}
  
navBar.addEventListener('click', dropDown);
navBar.addEventListener('mouseleave', noHover);

noHover();

Basic Vanilla JS Dropdown Menu


#4

hey… thanks.
But your solution has a little problem, if i click on the dropdown menu again it doesn’t disappear and if i click on the dropdowns while not leaving the navbar all of them are displayed


#5

Like I said, not perfect. Essentially what we have so far is a way to disable the CSS hover pseudo-class on the buttons so the dropdown doesn’t appear unless it is clicked. If you mouseover the menu the items can be clicked but the menu won’t disappear unless you mouseoff to the top, right, left or bottom, but not onto another button (in other words, off the navbar). You asked for a little vanilla JS and that is what you now have… Somewhere to start.


#6

yes great.
Now i want it to be just the way i planned.
Is there a way to do that cos I don’t want to use jQuery.


#7

Start by adding an event listener to dropMenu that listens for clicks on the menu. Give it the noHover callback so the menu disappears when an item is clicked.

dropMenu.forEach(el => {el.addEventListener('click', noHover)});

As for rolling off while still on the navbar, that will take a little bit of finaggling but it shouldn’t be that hard.

The REPL posted earlier updates automatically when changes are made so you can see the above addition in action right now.


#8

Update

This is working much more smoothly.

The mainstay of this code is the SitePoint scraped getEventTarget function, so please leave the attribution if you use this code.

//https://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/
const getEventTarget = e => {
  e = e || window.event;
  return e.target || e.srcElement;
};
const navBar = document.querySelector('.navbar');
const dropMenu = document.querySelectorAll('.dropdown-content');
const dropButton = document.querySelectorAll('.dropbtn');

const noHover = () => {
  dropMenu.forEach(el => {
    el.style.display = "none";
    //el.setAttribute('data-state', 'closed');
  });
}

const dropDown = (e) => {
  noHover();
  let $this = getEventTarget(e);
  if ($this.className === 'dropbtn') {
    let $that = $this.nextElementSibling;
    $that.setAttribute('data-state', $that.getAttribute('data-state') === 'open' ?
      'closed' : 'open'
    );
    $that.style.display = {
      'open': 'block',
      'closed': 'none'
    }[$that.getAttribute('data-state')];
  }
}
  
navBar.addEventListener('click', dropDown);
navBar.addEventListener('mouseleave', noHover);
dropMenu.forEach(el => {el.addEventListener('click', noHover)});

noHover();

I couldn’t get my own toggle to work and figured it had to be the way I was checking states, which is the approach I chose as a way to toggle the menu. Also, I wanted to stay away from toggling classes because that is frought with issues of its own (potential conflicts).

Did a little reading and came across an article that was right up my alley; lo and behold, it gave me the tweaks that were needed.

Toggling the data-state

With this approach, we are no longer reading in the current display state, but the arbitrary data-state attribute which is not so bound to JS inner workings. Getting a correct display state from that was easy peasy.

Or so it would seem. There are still wrinkles to iron out but I think this should get you on the right path. I’ll keep tinkering with it while one would hope you are digging into literature and documentation of anything that is new to you.

As of now I’m getting tied in knots and have invited another member to shed some light. Will resume tomorrow.


#9

you just saved me!
:joy::joy:
am grateful. Been trying to understand js since i started looking deep into it some 2 months ago.
I will input that into the project. Thanks


#10

one more thing, if the user clicks outside on any other part of the windowthe dropdown is still there


#11

There is still some wonkiness to sort out. I’ve got my hands full right now and need to pick this up later. Keep copies of different iterations and make note of the behavior of each. Eventually some clues will come forward. I’m still sure we’re on the right track.


#12

yeah… and was thinking if it couldn’t be any simpler?
like for a single dropdown i could do this:

function myFunction() {
    document.getElementById("myDropdown").classList.toggle("show");
}

// Close the dropdown if the user clicks outside of it
window.onclick = function(e) {
  if (!e.target.matches('.dropbtn')) {
    var myDropdown = document.getElementById("myDropdown");
      if (myDropdown.classList.contains('show')) {
        myDropdown.classList.remove('show');
      }
  }

#13

Owing to my desire to avoid class switching I stubbornly pursued the state approach. Eventually I’m sure a refined form will emerge that is just as simple as your example. We’ll never know if we don’t try.


#14

oh… okay.
Yeah i hope so. So which js framework do you use


#15

Most of my projects are trivial, exploring fundamental concepts. That means no framework, and only occasionally, jQuery.


#16

Update

I think we’ve made a breakthrough, and will leave the REPL alone for the next day or so.

//https://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/
const getEventTarget = e => {
  e = e || window.event;
  return e.target || e.srcElement;
};
const navBar = document.querySelector('.navbar');
const dropDown = document.querySelectorAll('.dropdown');
const dropMenu = document.querySelectorAll('.dropdown-content');
const noHover = () => {
  dropMenu.forEach(el => {
    el.style.display = "none";
  });
}
const noState = () => {
  dropMenu.forEach(el => {
    el.setAttribute('data-state', 'closed');
  });
}
const dropDownMenu = (e) => {
  let $this = getEventTarget(e);
  if ($this.className === 'dropbtn') {
    let $that = $this.nextElementSibling;
    $that.setAttribute('data-state', $that.getAttribute('data-state') === 'open' ?
      'closed' : 'open'
    );
    $that.style.display = {
      'open': 'block',
      'closed': 'none'
    }[$that.getAttribute('data-state')];
  }
}
const clear = () => {noHover(); noState()}
// run-once

dropDown.forEach(el => {
  el.addEventListener('mouseleave', clear);
});

dropMenu.forEach(el => {
  el.addEventListener('click', clear);
});

navBar.addEventListener('click', dropDownMenu);

clear();

Bear in mind that this works with the existing HTML. That is the crucial lesson here… Our script is connected to the DOM tree and must match up to the nodes, accordingly. Change the HTML and the script needs to be changed, too.

<div class="dropdown-content" data-state="closed">

Think what the implications are. It really is a ‘design with known constraints’ approach that we are left with. The HTML comes first, then the CSS, and then the behaviors. Browsers will expect ECMAScript in one JS form or another.

We still haven’t tested this from an accessibility view, but that is another story. Start with :focus. A visitor using the Tab key to navigate links will trigger the :focus pseudo-class in CSS. Our scirpt will need a way to monitor this, too. It’s not done, yet.


And now, this,

const noState = () => {
  dropMenu.forEach(el => {
    el.setAttribute('data-state', 'closed');
    el.style.display = "none";
  });
}

const dropDownMenu = (e) => {
  let $this = getEventTarget(e);
  if ($this.className === 'dropbtn') {
    let $that = $this.nextElementSibling;
    $that.setAttribute('data-state', $that.getAttribute('data-state') === 'open' ?
      'closed' : 'open'
    );
    $that.style.display = {
      'open': 'block',
      'closed': 'none'
    }[$that.getAttribute('data-state')];
  }
}

dropDown.forEach(el => {
  el.addEventListener('mouseleave', noState);
});

dropMenu.forEach(el => {
  el.addEventListener('click', noState);
});

navBar.addEventListener('click', dropDownMenu);

noState();

The noHover function has been done away with. Now we can focus further on the state, with the display concern being a tag-along.


#17

wow… that’s a lot of work.
Notice when once focus leaves, it hides


#18

If you go to the REPL you will see I have touched up the CSS so :focus has more bearing.

Run, then click on Home to give us a starting point. Mouse off then Tab. Now Tab again, notice the outline? Hit Enter to open the dropdown, then Tab through the menu and hit Enter to proceed on a link.

When first tabbing to and opening a menu, Enter again will close it and Tab will move to the next button. Shift tab works in reverse.

An improvement on this would be to include the arrow keys to shift around the Navbar, or up and down the menus.


#19

I asked for a roof, u gave me a house.
Like u said, a change in the css will affect it, when i moved to code to my project, i’m using a css framework called tachyons. A lil change and nothing worked. I’ll just stick to this css maybe.


#20

Hello, how is your day?
I have this error:
Uncaught TypeError: Cannot read property 'addEventListener' of null at dashboard.html:172
moved it to a simple workround first