SitePoint Sponsor

User Tag List

Results 1 to 21 of 21
  1. #1
    SitePoint Addict
    Join Date
    Jan 2009
    Posts
    390
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    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.

    Code:
    <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.

    Code:
    <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

    Code:
    <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/

  2. #2
    Community Advisor bronze trophy
    fretburner's Avatar
    Join Date
    Apr 2013
    Location
    Brazil
    Posts
    1,446
    Mentioned
    45 Post(s)
    Tagged
    13 Thread(s)
    Hi ttmt,

    Quote Originally Posted by ttmt View Post
    Code JavaScript:
    (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);
        }
    })();

    ...

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

    Uncaught TypeError: undefined is not a function
    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:

    Code JavaScript:
    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:
    Code JavaScript:
    function createHandler(n) {
    	return function() {
    		alert('you clicked ' + n);
    	}
    }
     
    for(var i=0; i<btn.length; i++) {
    	btn[i].addEventListener('click', createHandler(i));
    }
    "There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies."

  3. #3
    SitePoint Addict
    Join Date
    Jan 2009
    Posts
    390
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks fretburner , yes breaking it out into separate functions would be better

  4. #4
    Programming Since 1978 silver trophybronze trophy felgall's Avatar
    Join Date
    Sep 2005
    Location
    Sydney, NSW, Australia
    Posts
    16,869
    Mentioned
    25 Post(s)
    Tagged
    1 Thread(s)
    Quote Originally Posted by fretburner View Post
    Code JavaScript:
    	btn[i].addEventListener('click', createHandler(i));
    }
    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.
    Stephen J Chapman

    javascriptexample.net, Book Reviews, follow me on Twitter
    HTML Help, CSS Help, JavaScript Help, PHP/mySQL Help, blog
    <input name="html5" type="text" required pattern="^$">

  5. #5
    It's all Geek to me silver trophybronze trophy
    ralph.m's Avatar
    Join Date
    Mar 2009
    Location
    Melbourne, AU
    Posts
    24,319
    Mentioned
    462 Post(s)
    Tagged
    8 Thread(s)
    Quote Originally Posted by felgall View Post
    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.

  6. #6
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    6,055
    Mentioned
    219 Post(s)
    Tagged
    12 Thread(s)
    Hi Ralph,

    Quote Originally Posted by ralph.m View Post
    A little example of that would be really cool.
    Something like this:

    Code:
    <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());
    });

  7. #7
    It's all Geek to me silver trophybronze trophy
    ralph.m's Avatar
    Join Date
    Mar 2009
    Location
    Melbourne, AU
    Posts
    24,319
    Mentioned
    462 Post(s)
    Tagged
    8 Thread(s)
    Thanks so much Pullo. That's a nice example. I had tried querySelectorAll but didn't know what to do with it next.

  8. #8
    It's all Geek to me silver trophybronze trophy
    ralph.m's Avatar
    Join Date
    Mar 2009
    Location
    Melbourne, AU
    Posts
    24,319
    Mentioned
    462 Post(s)
    Tagged
    8 Thread(s)
    OK, even querySelector does the same thing:

    Code:
    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.

  9. #9
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    6,055
    Mentioned
    219 Post(s)
    Tagged
    12 Thread(s)
    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

  10. #10
    It's all Geek to me silver trophybronze trophy
    ralph.m's Avatar
    Join Date
    Mar 2009
    Location
    Melbourne, AU
    Posts
    24,319
    Mentioned
    462 Post(s)
    Tagged
    8 Thread(s)
    Quote Originally Posted by Pullo View Post
    Event capturing / bubbling are relevant if, for example, you have a element inside an element and both have an onClick event handler.
    Ah, thanks Pullo. That makes more sense. Thanks for the link, too. I'll check it out.

  11. #11
    Programming Since 1978 silver trophybronze trophy felgall's Avatar
    Join Date
    Sep 2005
    Location
    Sydney, NSW, Australia
    Posts
    16,869
    Mentioned
    25 Post(s)
    Tagged
    1 Thread(s)
    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.
    Stephen J Chapman

    javascriptexample.net, Book Reviews, follow me on Twitter
    HTML Help, CSS Help, JavaScript Help, PHP/mySQL Help, blog
    <input name="html5" type="text" required pattern="^$">

  12. #12
    It's all Geek to me silver trophybronze trophy
    ralph.m's Avatar
    Join Date
    Mar 2009
    Location
    Melbourne, AU
    Posts
    24,319
    Mentioned
    462 Post(s)
    Tagged
    8 Thread(s)
    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.

  13. #13
    Programming Since 1978 silver trophybronze trophy felgall's Avatar
    Join Date
    Sep 2005
    Location
    Sydney, NSW, Australia
    Posts
    16,869
    Mentioned
    25 Post(s)
    Tagged
    1 Thread(s)
    Quote Originally Posted by ralph.m View Post
    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.
    Stephen J Chapman

    javascriptexample.net, Book Reviews, follow me on Twitter
    HTML Help, CSS Help, JavaScript Help, PHP/mySQL Help, blog
    <input name="html5" type="text" required pattern="^$">

  14. #14
    It's all Geek to me silver trophybronze trophy
    ralph.m's Avatar
    Join Date
    Mar 2009
    Location
    Melbourne, AU
    Posts
    24,319
    Mentioned
    462 Post(s)
    Tagged
    8 Thread(s)
    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.

  15. #15
    SitePoint Guru bronze trophy
    Join Date
    Dec 2003
    Location
    Poland
    Posts
    930
    Mentioned
    7 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Pullo View Post
    Something like this:

    Code:
    <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());
    });
    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.

  16. #16
    Gre aus'm Pott gold trophysilver trophybronze trophy
    Pullo's Avatar
    Join Date
    Jun 2007
    Location
    Germany
    Posts
    6,055
    Mentioned
    219 Post(s)
    Tagged
    12 Thread(s)
    Quote Originally Posted by Lemon Juice View Post
    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:

    Code:
    document.querySelector("table").addEventListener('click', function(e){
      console.log("You clicked <" + e.target.nodeName.toLowerCase() + ">");
      e.target.style.backgroundColor = "red";
    });
    Code:
    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.

  17. #17
    SitePoint Guru bronze trophy
    Join Date
    Dec 2003
    Location
    Poland
    Posts
    930
    Mentioned
    7 Post(s)
    Tagged
    0 Thread(s)
    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!

    Quote Originally Posted by Pullo View Post
    For me, the second method is 98% slower than the first.
    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

  18. #18
    SitePoint Wizard bronze trophy Jeff Mott's Avatar
    Join Date
    Jul 2009
    Posts
    1,314
    Mentioned
    19 Post(s)
    Tagged
    1 Thread(s)
    For what it's worth, libraries like jQuery appear to have standardized on attaching events to containing blocks.

    Code JavaScript:
    $('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.
    "First make it work. Then make it better."

  19. #19
    SitePoint Guru bronze trophy
    Join Date
    Dec 2003
    Location
    Poland
    Posts
    930
    Mentioned
    7 Post(s)
    Tagged
    0 Thread(s)
    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.

  20. #20
    SitePoint Wizard Stomme poes's Avatar
    Join Date
    Aug 2007
    Location
    Netherlands
    Posts
    10,283
    Mentioned
    51 Post(s)
    Tagged
    2 Thread(s)
    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).

  21. #21
    It's all Geek to me silver trophybronze trophy
    ralph.m's Avatar
    Join Date
    Mar 2009
    Location
    Melbourne, AU
    Posts
    24,319
    Mentioned
    462 Post(s)
    Tagged
    8 Thread(s)
    Quote Originally Posted by Stomme poes View Post
    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)
    Yes, and the wrapping div doesn't have to have a lot of empty space in it.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •