3

Consider the following code:

function something(){
  ...
  document.addEventListener('click', clickHandler);
}

function clickHandler(e) {...}

something() is being called when clicking on an icon existing inside my page. So, by clicking on that icon, clickHandler() is being executed as well. That's totally unexplainable because the event listener is added afterwards. Any good idea?

Unknown developer
  • 5,414
  • 13
  • 52
  • 100
  • 1
    if you click the icon again, is `clickHandler()` executed twice? – Klaycon Jan 07 '20 at 17:42
  • 1
    Yes. It is executed. – Unknown developer Jan 07 '20 at 17:46
  • 1
    [events bubble down and up the DOM](https://stackoverflow.com/questions/4616694/what-is-event-bubbling-and-capturing) . you're registering a listener on `document`, which gets called no matter what element you click on . – Dan O Jan 07 '20 at 17:46
  • 1
    @DanO Events bubble upward only. They are captured down. ;) – Scott Marcus Jan 07 '20 at 17:50
  • @DanO Also, this doesn't explain why the handler is invoked twice (unless there is another handler being set up on the icon as well). Bubbling helps with event binding, but it doesn't affect how many times an event would be triggered. – Scott Marcus Jan 07 '20 at 17:52
  • You'll need to post the actual code that is behaving this way rather than pseudo-code. – Scott Marcus Jan 07 '20 at 17:53
  • @Scott fair point re: bubbling and capturing! and I assumed that `clickHandler` was indeed being registered in a second location, but I agree that we'll need to see more code to actually solve this problem. – Dan O Jan 07 '20 at 17:58

2 Answers2

3

Your answer is in the first line of your question:

something() is being called when clicking on an icon existing inside my page.

So, you must already have set up something as the click handler for your icon in order for that behavior to work. This means that every time you click the icon, you are triggering a click event that is handled by something, but you are also registering a completely different click event handler for the document.

Since events bubble, any clicks on the icon (which are in the document), will trigger that element's click event handler (something) and then the event will bubble up to the document, which will trigger its click event handler (clickHandler).

So nothing is out of sequence here. This is a simple case of one event handler setting up another and because the first handler is invoked before the second one would receive the event, the second binding happens in time for the document to handle the bubbled event.

Here's your code (slightly modified in order to run) with commentary:

document.querySelector("div").addEventListener("click", something);

function something(){
  console.log("function something has been invoked because the div was clicked");
  
  // Every time you click the div, you initiate a click event WITHIN the document
  // that will propogate upward to the document itself and the next line sets up
  // a handler at that level:
  document.addEventListener('click', clickHandler);
}

function clickHandler(e) {
  console.log(e.type + " triggered by: " + e.target + " handled at the document level");
}
div { background-color:#800080; color:orange;}
<p>First, click anywhere outside of the purple div and nothing will happen because the only thing
   that initially has a click event set up for it is the div. Then click on the div and you'll
   not only get the div's event handler to run, but because that handler sets up a click event
   handler for the document and the event hasn't bubbled up there yet, you'll get that same
   click event handled again, but by the document.
</p>
<p>
  Then, click outside of the div again. Because the document now has an event handler set up
  for it, the click will be handled there.
</p>
<div>Click me.</div>

If this behavior is not what you want, then you have a couple of options:

You can stop an event from bubbling any further (and therefore prevent it from getting to the document) using event.stopPropagation():

document.querySelector("div").addEventListener("click", something);

function something(e){
  // Handle the event here but don't allow it to go anywhere else
  e.stopPropagation();
  console.log("function something has been invoked because the div was clicked");
  
  // Every time you click the div, you initiate a click event WITHIN the document
  // that will propogate upward to the document itself and the next line sets up
  // a handler at that level:
  document.addEventListener('click', clickHandler);
}

function clickHandler(e) {
  console.log(e.type + " triggered by: " + e.target + " handled at the document level");
}
div { background-color:#800080; color:orange;}
<p>First, click anywhere outside of the purple div and nothing will happen because the only thing
   that initially has a click event set up for it is the div. Then click on the div and you'll
   not only get the div's event handler to run, but because that handler sets up a click event
   handler for the document and the event hasn't bubbled up there yet, you'll get that same
   click event handled again, but by the document.
</p>
<p>
  Then, click outside of the div again. Because the document now has an event handler set up
  for it, the click will be handled there.
</p>
<div>Click me.</div>

Handle all the clicks at the document level by just letting everything bubble up there and then respond to the events as you like by determining which element was responsible for triggering the event in the first place. This is called "event delegation". event.target lets you access that element:

// All clicks will eventually bubble up to the document
document.addEventListener('click', clickHandler);

function clickHandler(e) {
  console.log(e.type + " triggered by: " + e.target + " handled at the document level");
  
  // You can handle the event differently based on which element triggered it.
  if(e.target.classList.contains("notSpecial")){
    e.target.classList.add("special");
  }
}
.special { background-color:red; }
.as-console-wrapper { max-height: 1.5em !important; }
<h1>click me</h1>
<p>No, click me!</p>
<div>What about me?!</div>
<div class="notSpecial">Click me. I'm special</div>
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
0

It seems that handlers are still being called for the current event when you add this handler so it gets called, setting the useCapture value to true seems to stop it.

function something(){
  ...
  document.addEventListener('click', clickHandler, true);
}

useCapture Optional
A Boolean indicating whether events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. Events that are bubbling upward through the tree will not trigger a listener designated to use capture. Event bubbling and capturing are two ways of propagating events which occur in an element that is nested within another element, when both elements have registered a handle for that event. The event propagation mode determines the order in which elements receive the event. See DOM Level 3 Events and JavaScript Event order for a detailed explanation. If not specified, useCapture defaults to false.

Musa
  • 96,336
  • 17
  • 118
  • 137
  • You can also, if you like, use `stopPropagation()` to explicitly stop the event from continuing to bubble up. After adding the event-listener (wherever you need to add it ...), you prevent the current Event from ever triggering it. – Mike Robinson Jan 07 '20 at 18:17
  • Since not all events can be handled in the capture phase and since IE didn't support it early on, it's not widely used or super useful. `stopPropagation()` would solve this issue. But really, your answer doesn't address why this is happening in the first place. – Scott Marcus Jan 07 '20 at 18:19
  • Well, it's clear enough "why this is happening." The code adds an event-handler in a place where the event, which continues to propagate through every piece of code that might be interested in it, triggers the handler that was just added because it was added to a thing that had not yet been visited by the event-propagation walker. – Mike Robinson Jan 07 '20 at 18:21
  • @MikeRobinson It may be clear to you and I, but the fact that the question was asked means that it's not clear to the OP and perhaps others. So any "answer" should address the question. – Scott Marcus Jan 07 '20 at 18:32