addEventListener and closures

Hi all

This question follows on from another using the same code but it’s a completely different aspect so I
thought it woiuld be vest to start to new question abd not confuse the other post - (I’m not double posting)

So I hava a simple 4 buttons and a loop attaching a click event to each buttonn.


<div class="wrap">

  <button>One</button>
  <button>Two</button>
  <button>Three</button>
  <button>Four</button>

</div>


<script>

  (function init(){

    var btn = document.getElementsByTagName('button');

    for(var i=0; i<btn.length; i++){
      btn[i].addEventListener('click', function(){
        alert('you clicked '+i)
      });
    }
  })();

</script>

This will alert 4 for each button

I know I can fix this with a closure to capture each button separately.


<script>

  (function init(){

    var btn = document.getElementsByTagName('button');

    for(var i=0; i<btn.length; i++){
      btn[i].addEventListener('click', function(n){
        return function(){
          alert('you clicked '+ n)
        }
      })(i);
    }
  })();

</script>


This closure doesn’t work and I get an error in the console

Uncaught TypeError: undefined is not a function

If I do it with onclick it works


<script>

  (function init(){

    var btn = document.getElementsByTagName('button');

    for(var i=0; i<btn.length; i++){
      btn[i].onclick = (function(n){
        return function(){
          alert('you clicked '+ n)
        }
      })(i);
    }
  })();

</script>


Why does this work with onclick but not the addEventListener

I have a jsFiddle here - http://jsfiddle.net/562v6/

Hi ttmt,

You’re missing the opening parenthesis before your IIFE, and instead of the semi-colon at the end, you need a closing parenthesis for the addEventListener function:

btn[i].addEventListener('click', (function(n) {
	return function(){
	  alert('you clicked '+ n)
	}
})(i))

It’s kinda hard to see what’s going on there because of all the nesting. It would be easier to read (and to spot errors) if you separate things out a little:

function createHandler(n) {
	return function() {
		alert('you clicked ' + n);
	}
}
                
for(var i=0; i<btn.length; i++) {
	btn[i].addEventListener('click', createHandler(i));
}

Thanks fretburner , yes breaking it out into separate functions would be better

This will run createHandler straight away and attach what is returned to the listener.

Anyway, why use a loop. If you attach the eventListener to the div you can then easily determine which button was clicked within the code when the event runs rather than adding a separate listener to each.

A little example of that would be really cool. :slight_smile:

Hi Ralph,

Something like this:

<div class="wrap">
    <button>One</button>
    <button>Two</button>
    <button>Three</button>
    <button>Four</button>
</div>

var parent = document.querySelectorAll("div.wrap");
parent[0].addEventListener('click', function(e){
  console.log("You clicked button " + e.target.innerHTML.toLowerCase());
});

Thanks so much Pullo. That’s a nice example. I had tried querySelectorAll but didn’t know what to do with it next. :slight_smile:

OK, even querySelector does the same thing:

var parent = document.querySelector("div.wrap");
parent.addEventListener('click', function(e){
console.log("You clicked button " + e.target.innerHTML.toLowerCase());
});

Each time I see an example of addEventListener, it changes my concept of how it works. I’ve been trying to get a better understanding of capturing and bubbling of late, but am totally mystified over what they are all about, as the actual concept is never really explained. Is either of them involved here? I doesn’t seem to matter whether you use true or false here, so I guess they aren’t relevant. Just not quite sue why this example of your works.

EDIT: I see that if you click the div where there aren’t buttons the console logs the inner HTML of the div, which makes sense. Adding true or false at the end still doesn’t make a difference, though.

Hey Ralph,

Event capturing / bubbling are relevant if, for example, you have a element inside an element and both have an onClick event handler. If the user clicks on element2 he causes a click event in both element1 and element2.

When you use capturing, the event handler of element1 fires first, the event handler of element2 fires last.
When you use bubbling, the event handler of element2 fires first, the event handler of element1 fires last.

I took this from PPK’s article on event order. You can check it out here: http://www.quirksmode.org/js/events_order.html

Ah, thanks Pullo. That makes more sense. Thanks for the link, too. I’ll check it out. :slight_smile:

The part of capturing/bubbling that probably gets people most confused is that the true/false has no effect on how the particular event listener runs. It only affects when it runs inrelation to other event listeners that are triggered by the same event. Only when one event triggers multiple listeners does capturing/bubbling have any effect at all. When multiple listeners are triggered those defined as capturing run first starting from the outermost working in followed by those defined as bubbling starting from the inside working out. Where two listeners are attached to the same element and the same phase their order or running is undefined (but generally the first one added will run first). If any of the listeners execute a calcelBubble call then any listeners that have not yet been run that would normally have been triggered by the event will not run.

Thanks felgall. After reading Pullo’s linked page a few times, I’m getting a better understanding of it. Still haven’t fully grasped preventDefault() and cancleBubble yet, but getting a sense of what they do.

preventDefault works with events on elements that have a default action. For example <a href=“something.html”> has a default of transferring to the specified page. If you have JavaScript attached to that <a> tag and run preventDefault at the end of the JavaScript then it will not transfer to that page. Withthe submit button on a form the default action is to submit the form so if you use JavaScript validation you will use preventDefault to stop the form submitting when the script finds an error. It is the equivalent to using return false with event handlers.

cancelBubble relates to when you have multiple event listeners triggered by the same event. If the third of four event listeners runs cancelBubble then the fourth listener will not run. If the first of the four were to cancelBubble then none of the other three would run.

Thanks felgall. That’s much clearer now. I have used preventDefault before (kind of like return false) but got muddled when seeing it in the context of capturing and bubbling and thus was confusing it with cancelBubble. Need to do this stuff more consistently and not leave large intervals between. :rolleyes:

This is quite an original solution and I’d never thought I would solve it this way… In fact to me this looks a bit illogical and hackish to attach an event to a containing block when I want to capture events on the buttons. It could happen that a user clicks within the div but outside any of the buttons and then the event will still fire - so I need a way to filter those clicks in my handler - in this particular case the filtering will be very simple but what if I add some more elements inside the div in the future? And those elements are also buttons but I don’t want to set this event handler for them? Then I’ll need to remember to expand my filtering mechanism. And what if I wrap the button labels in other elements like <span> or add some <img>? Then the event target may not point the the <button> but to the <span> or <img>, which makes the filtering and detecting more complex.

To sum it up I think this is a bad idea from the maintenance point of view - the best way is to attach separate click handlers to each button, which keeps the code easy to maintain and read without any pitfalls. And we have the added benefit of being able to use this to access the clicked button within the handler - as opposed to having to decode/traverse DOM in order to get to it.

Essentially I agree with you, but in a case where performance is an issue, then the event delegation method is considerably faster.

You can check it out here: http://jsperf.com/event-delegation-vs-direct-binding

I made a test case with a table with 500 cells, which compares the two approaches:

document.querySelector("table").addEventListener('click', function(e){
  console.log("You clicked <" + e.target.nodeName.toLowerCase() + ">");
  e.target.style.backgroundColor = "red";
});
var cells = document.querySelectorAll("td");
for (var i = 0, len = cells.length; i < len; i++){
  cells[i].addEventListener('click', function(e){
    console.log("You clicked <" + e.target.nodeName.toLowerCase() + ">");
    this.style.backgroundColor = "red";
  });
}

For me, the second method is 98% slower than the first.

Sure, from performance point of view adding separate handlers will always be slower. But it’s pretty rare to have thousands of buttons on a page - a situation where it begins to matter!

It’s not entirely fair to say the second method is 98% slower than the first because you are comparing adding 1 event handler against adding n handlers - in this case 500. If, in the 2nd round, you were adding 5000 handlers then the difference would probably be 99.x%. If you were adding 2 handlers then the difference would be ~50% - and if you were comparing the difference for 4 or 10 buttons you would need to use a microsecond timer to even be able to benchmark it since millisecond resolution will not be enough :slight_smile:

For what it’s worth, libraries like jQuery appear to have standardized on attaching events to containing blocks.

$('div.wrap').on('click', 'button', function() {
  // In jQuery callbacks, you can refer to the target element with "this"
  // In vanilla JS, you'd probably have to use e.target

  console.log("You clicked button " + this.innerHTML.toLowerCase());
});

Strictly speaking, an event is triggered for every click, even if it wasn’t on a button, but those events are filtered, such as above, where we filter using the selector “button”, so that our callback fires only when the element we wanted to target is clicked.

My impression is that the JavaScript community is increasingly favoring events on containing blocks (most often referred to as event delegation). The two benefits I’m aware of are 1) you attach just one handler instead of a dozen or however many, and 2) the event is “live”. By “live” I mean that if you were to add or remove button elements, you wouldn’t have to worry about un-binding the old button elements and re-binding the new ones. Instead, you just add them like normal, and whatever button element that happens to be inside the container will bubble its events.

It looks like jQuery’s on() method is filtering the event and assigns the desired element to this. In vanilla JS this would refer here to div.wrap so we would need to use some hand-made filtering mechanism.

Having “live” events is a nice benefit, indeed, provided it is the desired behaviour.

Live is good, but I like the not-having-a-bazillion-listeners.
There’s a sweet spot for it though: you want to be far enough away from the targets that you can group all their events in one listener (like the div parent of the buttons), but not so far away that loads of bubbling levels have to be gone through first (delegating all listeners to, say, the document or body).