I have this mobile link menu that I want to open with a button and be able to close by all means possible. Either by clicking the same button again, clicking one of the menu links, or anywhere else on the page. Meaning that once the menu is open, every single pixel on the screen is a valid candidate for detecting and closing the menu.
That's why I set up a listener for the menu button to toggle the menu. And once the menu is visible, set up another one-time-listener on the whole document to toggle it hidden again:
document.querySelector('#box').addEventListener('click', () => {
console.log("box clicked");
document.addEventListener('click', () => {
console.log('everything clicked');
}, {once: true})
})
<div id="box" style="width: 200px; height: 200px; background-color: red"></div>
I have this mobile link menu that I want to open with a button and be able to close by all means possible. Either by clicking the same button again, clicking one of the menu links, or anywhere else on the page. Meaning that once the menu is open, every single pixel on the screen is a valid candidate for detecting and closing the menu.
That's why I set up a listener for the menu button to toggle the menu. And once the menu is visible, set up another one-time-listener on the whole document to toggle it hidden again:
document.querySelector('#box').addEventListener('click', () => {
console.log("box clicked");
document.addEventListener('click', () => {
console.log('everything clicked');
}, {once: true})
})
<div id="box" style="width: 200px; height: 200px; background-color: red"></div>
When the first eventlistener detects a click, it runs the callback function. Inside that function, I've set up another onClick listener that fires instantaneously like it's piggybacking off the other click event!
I understand that due to propagation nested listeners will both be activated, but what I'm expecting is that the very first time a click is registered only the "box clicked" will run since once inside the callback function, the event has passed and the second listener needs to wait for the next click event to be able to detect it. But instead, it adds a listener and detects it immediately.
My expected logs (from sequentially clicking on the box):
-
- box clicked
-
- box clicked
- everything clicked
-
- box clicked
-
- box clicked
- everything clicked
-
- box clicked
-
- box clicked
- everything clicked
etc...
Share Improve this question edited Aug 24, 2021 at 21:17 Nermin asked Aug 17, 2019 at 0:41 NerminNermin 94110 silver badges26 bronze badges 1- javascript.info/bubbling-and-capturing – kip Commented Aug 17, 2019 at 0:57
2 Answers
Reset to default 4You're setting the box up to have a click
event as well as the document
. But, the box is part of the document, so when you click the box, the event propagates to the document
as well. This is because events propagate and as soon as you click the box, you've set up the document handler and it happens so quickly that the new handler is in place before the initial click event propagates to the document.
If you don't want box clicks to do this, use the stopPropagation()
method in that handler.
But, you've got another issue with this code. Because you are setting up a document event handler when the box gets clicked, it will set up an additional handler on the document EVERY TIME you click the box.
Try it below by clicking the box 3 times and then click outside of the box once.
document.querySelector('#box').addEventListener('click', (evt) => {
evt.stopPropagation();
console.log("box clicked");
document.addEventListener('click', () => {
console.log('outside of box clicked');
});
})
<div id="box" style="width: 200px; height: 200px; background-color: red">Click me to fire my handler and set up the document handler.</div>
To get around this, it's best to set up a variable to keep track of whether the document handler has already been set:
let boxClicked = false;
document.querySelector('#box').addEventListener('click', (evt) => {
evt.stopPropagation();
console.log("box clicked");
// Only set the next handler if it's the first time you've clicked the box
if(!boxClicked) {
document.addEventListener('click', () => {
console.log('outside of box clicked');
});
}
boxClicked = true;
})
<div id="box" style="width: 200px; height: 200px; background-color: red">Click me to fire my handler and set up the document handler.</div>
Or (and this is more resource intensive), remove the old handler before adding a new one. This technique requires that the callback function be set up as a named function. Your edited question shows you using the {once:true}
options argument for addEventLister()
, which solves the problem, but that code essentially does the following, which (as I say) is more resource intensive because constantly adding and removing listeners uses memory.
document.querySelector('#box').addEventListener('click', (evt) => {
evt.stopPropagation();
console.log("box clicked");
// Remove the first handler
document.removeEventListener("click", handleDocClicked)
// Then set up a new one
document.addEventListener('click', handleDocClicked);
});
function handleDocClicked(){
console.log('outside of box clicked');
}
<div id="box" style="width: 200px; height: 200px; background-color: red">Click me to fire my handler and set up the document handler.</div>
I created this solution inspired and based on Scott's solution, but with the additional last tweaks to make the solution follow the expected behavior precisely.
let toggleBox = false
document.querySelector('#box').addEventListener('click', (evt) => {
console.log("box clicked");
if (!toggleBox) {
evt.stopPropagation();
}
toggleBox = !toggleBox
document.addEventListener('click', handleDocClicked, {once:true});
});
function handleDocClicked(){
console.log('everything clicked');
toggleBox = false
}
<div id="box" style="width: 200px; height: 200px; background-color: red">Click me to fire my handler and set up the document handler.</div>