Saturday, May 16, 2009

Referencing Local Variables in jQuery Callback Functions

One day I was working on a CRUD application, which contains a lot of form fields and controls. Each of them requires a mouse over event handler to display a tooltip message. After lots of copy’n’pasting, I decided to refactor the repetitive event registration code out and put them into a loop.

To test out my idea, I developed a very simple page with just three <div>s:

<div id="div1">Click me</div>
<div id="div2">Click me</div>
<div id="div3">Click me</div>

and a list of messages indexed by the <div>s’ ID:

var messages = new Object();
messages['div0'] = 'hello'; 
messages['div1'] = 'bonjour'; 
messages['div2'] = 'ciao';

When someone click on one of the <div>’s region, I would like to show a popup an dialog and display the message associated with it’s ID. Since my goal was to eliminate repetitive lines of code, I put the event registration code in a loop:

for(var i=0; i<3; ++i) {
  $("#div" + i).click(function() { 
    alert(messages['div' + i]); 
  });
}

When I tried this out, instead of showing the right message, the alert box always shows ‘undefined’:

image

After a lot of head scratching, I finally realised that the problem is with this line of code referencing the loop variable i:

alert(messages['div' + i]);

The computer scientists’ way of describing this is that it formed a “Closure” referencing the variable i. Since this line of code sits inside the callback function,  the ‘div’ + i statement wasn’t evaluated to ‘div0’, ‘div1’ and ‘div2’ in each loop iteration as I expected. Instead, because each of the callback function holds a reference to the variable i, the ‘div’ + i statement was using the final value of i, which is 3 in this case.

This can be proven by changing the alert statement to show the value of i, which will always be 3 when any of the <div>s is clicked.

alert(i);

I Googled around for solutions but most of them are pretty complicated but eventually I found an answer in the jQuery reference document. All jQuery objects have a data() method, which allows you to bind any data to it. The bound data will be instance specific and the value will be evaluated at the binding time.

Therefore, to fix my code I just have to change the event registration code to:

for(var i=0; i<3; ++i) {
  $('#div' + i).data('divID', 'div' + i);
  $('#div' + i).click(function() {
    alert(messages[$(this).data('divID')]); 
  });
}

So in each iteration of the loop, the value ‘div0’, ‘div1’ and ‘div2’ are stored into the ‘divID’ data section of the respective <div> objects and then the callback function uses the stored value to find out which message to display when they are clicked.

The final solution looks like this:

$(function() {
  var messages = new Object();
  messages['div0'] = 'hello'; 
  messages['div1'] = 'bonjour'; 
  messages['div2'] = 'ciao';
         
  for(var i=0; i<3; ++i) {
    $('#div' + i).data('divID', 'div' + i);
    $('#div' + i).click(function() { 
      alert(messages[$(this).data('divID')]); 
    });
  }      
});

4 comments:

  1. Thanks, this helped me figure out why my callbacks weren't working.

    ReplyDelete
  2. Thank you very much! Nice solution and not complicated at all.

    ReplyDelete
  3. This not the first time when I face the problem, but this is the best solution that I found so far.
    Thanks a lot!

    ReplyDelete