I often find myself wanting to execute a particular action when the user clicks outside of an element. For example when dismissing a modal window.
Right now I use something along these lines:
<!-- gets run on ANY click ANYWHERE -->
<div data-action="click@window->myController#myActionIfOutsideClick"></div>
// Stimulus action
myActionIfOutsideClick(event) {
event.preventDefault()
// Ignore event if clicked within element
if(this.element === event.target || this.element.contains(event.target)) return;
// Execute the actual action we're interested in
this.myAction()
}
This works, but tightly couples the action to the event. Which I think is against the spirit of Stimulus?
Anytime I want to handle the “click outside element” event, I need to make sure I have an additional action that checks the user isn’t clicking on the element itself.
I wonder how others approach this and whether it’s worth considering a custom “clicked outside element” event to the Stimulus library? So we could do something like this instead:
Thanks for raising this question I used it as an excuse to add a new behavior to stimulus-use to handle similar cases.
This approach is slightly different as it removes the markup requirements from your HTML.
Thanks Adrien. This is really helpful. I appreciate you spending your time on this.
If I understand your approach correctly, this would require us to use an action called clickOutside, right?
That means we’ll be declaring all behavior in the clickOutside action, rather than a data-action attribute which is typically the case in Stimulus. (e.g. data-target="clickOutside->modal#close")
It does seem very pragmatic though. I tried building a custom “clickOutside” event so we could bind the target/action as above, but I couldn’t get it to work.
FYI I released a new version of Stimulus Use and you now have a click outside mixin available
import { Controller } from 'stimulus'
import { useClickOutside } from 'stimulus-use'
export default class extends Controller {
connect() {
useClickOutside(this)
}
clickOutside(event) {
// example to close a modal
event.preventDefault()
this.modal.close()
}
}
I’m trying to replicate an old and convoluted hamburger menu with overlay elements and whatnot, and I’ve managed to strip it way down, thanks to the useClickOutside part of Stimulus use (thanks, @adrienpoly !). But one cool thing about the old convoluted markup/css was the fact that the invisible overlay element used as a click-outside target made it easy to style it with a pointer cursor.
Can I replicate this with useClickOutside without extra divs?
I already toggle a class on the body element that indicates the menu is open, so I tapped into that existing class with cursor: pointer;, but it feels wrong to add a pointer to the entire body, even if it’s just for a modal that will stay closed 95 percent of the time. Is there a better way?
Calling this globally doesn’t feel right – especially when there are other interactions on the page for the user. Realize something may be off about this entire approach.