77

I'm having several div's #mydiv1, #mydiv2, #mydiv3, ... and want to assign click handlers to them:

$(document).ready(function(){
  for(var i = 0; i < 20; i++) {
    $('#question' + i).click( function(){
      alert('you clicked ' + i);
    });
  }
});

But instead of showing 'you clicked 3' when click on #mydiv3 (as for every other click) I get 'you clicked 20'. What am I doing wrong?

givanse
  • 14,503
  • 8
  • 51
  • 75
Philip
  • 3,470
  • 7
  • 29
  • 42

6 Answers6

140

It's a common mistake to create closures in loops in Javascript. You need to have some sort of callback function like this:

function createCallback( i ){
  return function(){
    alert('you clicked' + i);
  }
}

$(document).ready(function(){
  for(var i = 0; i < 20; i++) {
    $('#question' + i).click( createCallback( i ) );
  }
});

Update June 3, 2016: since this question is still getting some traction and ES6 is getting popular as well, I would suggest a modern solution. If you write ES6, you can use the let keyword, which makes the i variable local to the loop instead of global:

for(let i = 0; i < 20; i++) {
  $('#question' + i).click( function(){
    alert('you clicked ' + i);
  });
}

It's shorter and easier to understand.

Harmen
  • 22,092
  • 4
  • 54
  • 76
  • 7
    Actually as an explanation it should be written that the anonymous function referencing "i" is only executed in context of the loop not immediatly run. This means, when the onclick event happens, "i" is actually at the end condition of the loop. You could circumvent this by passing "i" as a local parameter (as this callback solution does). – hurikhan77 Nov 08 '10 at 13:19
  • 3
    I *really* wouldn't call `callback` "callback". It isn't a callback. It's a factory for *creating* callbacks. – T.J. Crowder Jan 16 '12 at 14:26
  • Really useful answer. Had this issue as well and was able to modify your solution to create a bind click event each time the loop passed through. – Ian Aug 06 '13 at 18:21
  • it will always be twenty. caused by clientsided increment. – de_nuit Oct 29 '14 at 12:56
12

To clarify, i is equal to 20 because the click event won't have fired until after the loop has finished.

Donald
  • 1,718
  • 10
  • 18
7
$(document).ready(function(){
  for(var i = 0; i < 5; i++) {
   var $li= $('<li>' + i +'</li>');
      (function(i) {
           $li.click( function(){
           alert('you clicked ' + i);
         });
      }(i));
      $('#ul').append($li);
  }
});
user1490857
  • 71
  • 1
  • 1
6

Using on to attach the 'click' handler you can use the event data in order to pass your data like in:

for(var i = 0; i < 20; i++) {
  $('#question' + i).on('click', {'idx': i}, function(e) {
    alert('you clicked ' + e.data.idx);
  });
}

//
// let's creat 20 buttons
//

for(var j = 0; j < 20; j++) {
 $('body').append($('<button/>', {type: 'button', id: 'question' + j, text: 'Click Me ' + j}))
}

//
// Passing data to the handler
//
for(var i = 0; i < 20; i++) {
  $('#question' + i).on('click', {'idx': i}, function(e) {
    console.log('you clicked ' + e.data.idx);
  });
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
gaetanoM
  • 41,594
  • 6
  • 42
  • 61
4

You can get by with assigning the click handler once (or at least not making many unnecessary closures). Put all the divs in one class mydivs, then:

$(document).ready(function(){
    $('.mydivs').click(function(){
        // Get the number starting from the ID's 6th character
        // This assumes that the common prefix is "mydiv"
        var i = Number(this.id.slice(5));

        alert('you clicked ' + i);
    });
});

This looks at the element's ID to get its number, using the slice string method to strip the initial letters off.

Note: It may be better to use

$('#divcontainer').on('click', '.mydivs', function(){

instead of

$('.mydivs').click(function(){
PleaseStand
  • 31,641
  • 6
  • 68
  • 95
2

Generally, if you are looking to assign click handles to a large number of items, you want to have a container (higher level div) that interprets the clicks for you, as the click bubbles up from the dom.

<div id="bucket">
    <span class="decorator-class" value="3">
    ...
</div>

<script>
   $(document).ready(function(e){
      $("#bucket").live('click', function(){
         if(e.target).is('span'){
            alert("elementid: " + $(e.target).val());
         }
      }
   }
<script>
stancoffyn
  • 47
  • 2