2

I am just starting off teaching myself Javascript after learning some basics in a university class a year ago. For a web app I am supposed to write, I need to allow users to "select" rows of a table, preferably by clicking on them. I have a strong Python background and my approach probably reeks of GTK paradigms. I have a nagging fear that I'm "reinventing the wheel" somehow by trying to do this when there is a much more standard or accepted way, but like I said, don't know much Javascript yet. My Javascript code to try and do this is:

//Get list of rows in the table
var table = document.getElementById("toppings");
var rows = table.getElementsByTagName("tr");

var selectedRow;

//Row callback; reset the previously selected row and select the new one
function SelectRow(row) {
    if (selectedRow !== undefined) {
        selectedRow.style.background = "#d8da3d";
    }
    selectedRow = row;
    selectedRow.style.background = "white";
}

//Attach this callback to all rows
for (var i = 0; i < rows.length; i++) {
    var idx = i;
    rows[idx].addEventListener("click", function(){SelectRow(rows[idx])});
}

This apparently doesn't work because after the loop exits idx is equal to rows.length - 1 so the row argument in SelectRow is rows[rows.length - 1], which is always the last row; that is, the values of i are not "saved" as arguments to the event listener callback. How can I call an event listener with its own saved arguments? This thread seems to give a way to do this with the onclick property, but I understand that this is deprecated and addEventListener (or some monstrous IE-compatible addEvent library) is now the "correct" way to do it.

Community
  • 1
  • 1
dpitch40
  • 2,621
  • 7
  • 31
  • 44

2 Answers2

2

You are having a closure problem. Here's a reference: JavaScript closure inside loops – simple practical example

Try this:

for (var i = 0; i < rows.length; i++) {
    (function (idx) {
        rows[idx].addEventListener("click", function() {
            SelectRow(rows[idx]);
        });
    })(i);
}

http://jsfiddle.net/2Vs5w/2/

Of course, you don't actually need to pass rows[idx] to SelectRow - you could pass this and not have to deal with creating a closure to capture the value of i at that time. So something like this:

for (var i = 0; i < rows.length; i++) {
    rows[idx].addEventListener("click", function() {
        SelectRow(this);
    });
}

http://jsfiddle.net/2Vs5w/3/

In the examples, I included a function for effectively attaching an event to an element:

function addEvent(element, evt, callback) {
    if (element.addEventListener) {
        element.addEventListener(evt, callback, false);
    } else if (element.attachEvent) {
        element.attachEvent("on" + evt, callback);
    } else {
        element["on" + evt] = callback;
    }
}

Tries to use the modern standard addEventListener, then attempts to use IE's way - attachEvent, then the DOM0 way - element.onevent. There are several places to get this function from, and probably even better versions, but this should work for now :)

Community
  • 1
  • 1
Ian
  • 50,146
  • 13
  • 101
  • 111
  • Agh...I can't keep track of that many braces and parentheses...I can see why everyone writes their own addEvent function. – dpitch40 Mar 22 '13 at 14:21
  • @dpitch40 Haha yeah it's a pain, but there's always libraries that do it reliably for you, like jQuery. Here's John Resig's example: http://ejohn.org/projects/flexible-javascript-events/ – Ian Mar 22 '13 at 14:24
1

You have found the usual JavaScript gotcha! :)

  1. Variables have function scope
  2. Closures maintains a reference to the parent scope (not a copy of variable values).

If you sum up 1 + 2 you'll figure out why this line is wrong:

   rows[idx].addEventListener("click", function(){SelectRow(rows[idx])});

The idx in the closure is a reference to the parent scope (2), at the end of the loop idx = rows.length why? It doesn't matter that you put the var inside the loop, declarations are at function level (1).

You can use @Ian solution to copy idx.

NOTE 1: Events handlers receives an event object, you can use event.target to get the object that fired the event, in that way you don't need to pass row[idx]:

   rows[idx].addEventListener("click", function(evt){SelectRow(evt.target)});

NOTE 2: Usually attaching event handlers to each row can be inefficient, try by attaching the event handler to the table instead

Diego
  • 4,353
  • 1
  • 24
  • 22