DEV Community

loading...
Cover image for The "Off" Click

The "Off" Click

jh3y profile image Jhey Tompkins Originally published at jhey.dev on ・4 min read

Clicking anywhere off an element in Javascript ✋

I don’t know the technical term for this scenario 😅 We’ve likely all encountered it though. That scenario where you’ve bound a click to open or activate something. But, you also want a click bound to clicking anywhere else that closes it.

What is the technical term for that? I’m going to call it the "off" click.

Consider this common example using a side menu. You click the menu button to open the menu. When you go to click off it, you expect it to close. It shouldn’t be the case that it only closes when you click the menu button again.

For those in camp TL;DR , the solution is bind an "Off" click method to the document within your initial click handler. This gets added and removed when necessary. Here’s a demo!

Before we go any further. This isn’t just applicable to the side menu scenario. It could be used in other scenarios you may come across. It is also true, that we could use other methods to close the sliding menu such as a clickable overlay that fills the rest of the page.

A Side Menu

For our example, we are going to use a basic side menu that slides in and out. The menu contains items that allow the user to change the background color of the page.

Basic opening and closing

To open and close the side nav, we will apply a class to the menu element. The class will dictate the transform of the menu element sliding it right and left.

So what might the code look like for this? In the most basic implementation we can toggle the class on the menu element.

const OPEN_CLASS = 'menu--open'
const menu = document.querySelector('.menu')
const menuBtn = menu.querySelector('.menu__button')
// Most basic method
menuBtn.addEventListener('click', () => menu.classList.toggle(OPEN_CLASS))
Enter fullscreen mode Exit fullscreen mode

But, this isn’t ideal. We can open the menu, but the only way to close it is by clicking the menu button again.

Showing how "off" click does nothing

That won’t do 👎

Introducing the "Off" click

How do we deal with this? We need an "Off" click.

Instead of only toggling the class, we can also bind an event listener to the document at the same time. The function we bind to can then close the menu. This means clicking anywhere on the document will close the menu.

const offClick = () => {
  menu.classList.toggle(OPEN_CLASS)
  document.removeEventListener('click', offClick)
}
const handleClick = e => {
  menu.classList.toggle(OPEN_CLASS)
  if (menu.classList.contains(OPEN_CLASS)) {
    document.addEventListener('click', offClick)
  }
}
menuBtn.addEventListener('click', handleClick)
Enter fullscreen mode Exit fullscreen mode

Wait, that doesn’t work…

Menu not opening

The reason? Event propagation. We bind the document click in the same instance as clicking to open the menu, the event propagates. That means the menu opens and closes so quick we never see it. Let’s fix that!

const offClick = () => {
  menu.classList.toggle(OPEN_CLASS)
  document.removeEventListener('click', offClick)
}
const handleClick = e => {
  e.stopPropagation()
  menu.classList.toggle(OPEN_CLASS)
  if (menu.classList.contains(OPEN_CLASS)) {
    document.addEventListener('click', offClick)
  }
}
menuBtn.addEventListener('click', handleClick)
Enter fullscreen mode Exit fullscreen mode

To fix it we could use stopPropagation. This will stop the event bubbling that makes the menu close when it should be open.

Menu opening and closing

But the use of stopPropagation could introduce other pitfalls that we want to avoid 👻

Be wary of using stopPropagation

The use of stopPropagation can be risky and can introduce bugs if not used with care 🐛 In this solution, we are only using it on the click handler of the menu button. We are stopping the click event from bubbling up.

But using stopPropagation means that we create a clicking deadzone. How? Later down the line, we may decide to add an event handler to a parent of the menu button. But if we click the menu button, the event wont propagate. That means the new event handler wouldn’t fire 😢

Menu Button creating a deadzone click. Clicking the body increases a number on the page. But, clicking the button does nothing.

Comparing events

One solution is to compare the event objects. We can encapsulate the logic within our menu button click handler.

const handleClick = e => {
  const offClick = evt => {
    if (e !== evt) {
      menu.classList.toggle(OPEN_CLASS)
      document.removeEventListener('click', offClick)
    }
  }
  if (!menu.classList.contains(OPEN_CLASS)) {
    menu.classList.toggle(OPEN_CLASS)
    document.addEventListener('click', offClick)
  }
}
menuBtn.addEventListener('click', handleClick)
Enter fullscreen mode Exit fullscreen mode

This way, we aren’t stopping the event propagation.

No more deadzone. Clicking the button triggers both actions. It increases the number but also opens and closes the menu.

This way we can still propagate the event but make sure that we don’t fall into the instant open and close issue.

Can we extract that logic?

Yes. It’s unlikely you’ll have to cater for the "Off" click in several places across your app, but it won’t hurt to refactor.

const addOffClick = (e, cb) => {
  const offClick = evt => {
    if (e !== evt) {
      cb()
      document.removeEventListener('click', offClick)
    }
  }
  document.addEventListener('click', offClick)
}
Enter fullscreen mode Exit fullscreen mode

We could now apply the behavior across different parts of our app 🎉

const handleClick = e => {
  const toggleMenu = () => menu.classList.toggle(OPEN_CLASS)
  if (!menu.classList.contains(OPEN_CLASS)) {
    toggleMenu()
    addOffClick(e, toggleMenu)
  }
}
Enter fullscreen mode Exit fullscreen mode

That’s it!

A quick look at the "Off" click scenario. We’ve covered how to handle it whilst avoiding the use of stopPropagation.


Any good? Let me know! Let's connect!

Discussion (3)

pic
Editor guide
Collapse
janpauldahlke profile image
jan paul

while reading iam thinking about :not() CSS pseudo-class which should basically achieve the same effect, but less writing js. but maybe i understood "off"click wronk.

but: edit: i am to lazy to code you an example atm.

Collapse
pomfrit123 profile image
***

great, original content, finally :)

Collapse
jh3y profile image
Jhey Tompkins Author

Don't worry. You'll never find me publishing "Top Lists" 😅