javascript - giving .onClick to each cell in a table - javascript

I have a problem - I want to add an .onClick event to each cell of a dynamically generated table while I am creating it.
cell.onclick = function(){
mouseClick(row_number,i, cell);
};
However, it appears that the event is just added to the last cell in each row. How is it possible? Is it somehow overriding the previous ones? And how to make it work?
function drawRow(rowData, length, keys, row_number){
//rowData - object where the row data is stored, length-row length, keys - object with table keys stored, row_number - number of the row
var rowData=rowData;
var row = document.createElement("tr");
for(i=0; i<length; i++){
var cell = document.createElement("td");
console.log("Creating event for "+i+" .");
cell.onclick = function(){
mouseClick(row_number,i, cell);
};
var key = keys[i];
var cell_text = document.createTextNode(rowData[key]);
cell.appendChild(cell_text);
row.appendChild(cell);
}
return row;
}

You are seeing one of the most common problems in JavaScript-land.
Your onclick functions are not called until after that for-loop is done. When the for-loop is done, the value of i is that of length. Only then do you try to read it. In other words, every single one of your callbacks say
function () {mouseClick(row_number, i, cell}
By the time you finally execute the callback, the value of i is its last value.
If you are using ES6, instead write
for (let i=0; i<length; i++)
The let gives each invocation of the block its own variable i. In your code you only have one i shared among all the cells. That is why all cells are appearing to do the same thing!
If you are still in the ES5 game, replace the inner portion of your code with
cell.onclick = (function (j) {
return function () {
mouseClick(row_number, j, cell);
}
})(i);
This gives to each of your callback functions a copy of i at each point of the loop. The first time through the loop, i has a value of 0, and the callback you get is
function () {mouseClick(row_number, 0, cell}
The second iteration of the loop produces
function () {mouseClick(row_number, 1, cell}
and so on!
Pretty cool...but the ES6 way with let does the same thing, a bit more nicely.
(Alternatively, there are ways to do this thing with the forEach method on arrays. These also ensure the loop iterations do not share the index variable.)

Related

Calling a variable inside a loop when said variable is defined in a separate loop

I'd like someone - preferably Jon Skeet but I'll accept answers from mere mortals - to explain the following basic JavaScript scenario to me. I have a function similar to the following:
myFunction()
{
var row;
for (var i = 0; i < collectionView.itemsAdded.length; i++)
{
row = this.collectionView.itemsAdded[i];
}
for (var i = 0; i < collectionView.itemsAdded.length; i++)
{
// this always logs as the last value it was given from the first loop. I expected it to give me the same variety of values it gave me in the first loop.
row.property1 = true;
}
}
Unexpectedly, the second use of row always results in whatever the final value the first for loop gave it. i.e. if collectionView.itemsAdded.length is 45, row will log as 44 (bc zero index) and stay 44 on every iteration of the second loop.
I falsely expected that calling row in the second for loop would be enough to give me the same results row gave me in the first for loop. I thought that for each iteration of the second loop, row would log as a different item - instead, it logs every time as this.collectionView.itemsAdded[44] which it got from the first loop.
In my head, I get it. But can anyone explain further what's happening here? If row is supposedly this.collectionView.itemsAdded[i] - why isn't it transferable to another loop? Why must I again declare row = this.collectionView.itemsAdded[i] in the second for loop if I want the same values?
row is just holding a reference to an object. In first loop, it keeps on changing. After the loop ends, the value of row is not updated. So, in the second loop, the operations are done on the same object i.e. the last value that was assigned to row.
Row is not an array. In the first loop, row is assigned in every iteration which is why it equals this.collectionView.itemsAdded[44]; at the end.
In the second loop, what you are doing is assigning :
this.collectionView.itemsAdded[44].property1 = true;
If you want to assign every item of your list to true, row should be an array and in the frst loop you would need to push every item like this:
row.push(this.collectionView.itemsAdded[i]);
myFunction()
{
var row;
for (var i = 0; i < collectionView.itemsAdded.length; i++)
{
// this row will assign a new value to row in each iteration
row = this.collectionView.itemsAdded[i];
}
// first run: i = 0 ||| row = this.collectionView.itemsAdded[0]
// second run: i = 1 ||| row = this.collectionView.itemsAdded[1]
//...
// last run: i = 44 ||| row = this.collectionView.itemsAdded[44]
// this loop is not executing until the first loop is finished this is why in
// the first iteration of this loop the value is the last value assign to
// row in the first loop
for (var i = 0; i < collectionView.itemsAdded.length; i++)
{
// this row is just using the last assign value to row (from the last
// iteration in the first loop
row.property1 = true;
}
// first run: i = 0 ||| row = this.collectionView.itemsAdded[44]
//...
// on each iteration you'r just using the last value assign to row from the
// first loop
//...
// last run: i = 44 ||| row is still = this.collectionView.itemsAdded[44]
}
It's very simple. The first loop is assigning the row in each iteration of i leaving row assigned with the last i as the last row. In the second loop you're using the saved row variable assigned in the last iteration of the first loop which is the last row.
You need to declare row as an array instead of object. Object wil override the value when next value with same key comes, but array with add it in queue.
On the first iteration please push the required value to row
let row=[]
row.push()

Loop and Functions

I'm having troubles gathering information about clicked eventListeners.
I have this loop which builds an array:
myButtonList = document.getElementsByTagName('a');
myAnchorList = [];
for (i=0; i < myButtonList.length;i++) {
if (myButtonList[i].getAttribute('class') == 'flagged') {
myAnchorList.push(myButtonList[i]);
}
}
For each <a> put into myAnchorList array, I also create another array storing other informations from the same tag (classe and other atrributes).
Here's where I'm struggling. I'm trying to set up an eventListener to send me back those information when those <a> are being clicked. But somehow, the fact that I create a function (for the eventListener) within a loop breaks everything.
for (i=0; i < myAnchorList.length; i++) {
myAnchorList[i].addEventListener("click", function(i){
console.log(alpha+' - '+beta[i]+" - "+charlie[i]);
});
}
My values will either be undefined or some other values which will be the same for each buttons I clicked. alpha is working well as it doesn't depend on any iteration of the loop, but not the others.
Can anybody see what I'm doing wrong here?
for (var i = 0; i < myAnchorList.length; i++) {
(function (i) { //Passes i to your function
myAnchorList[i].addEventListener("click", function () {
console.log(alpha+' - '+beta[i]+" - "+charlie[i]);
});
})(i);
}
The variable "i" in closure that you created in the loop will always retrieve the last value(myAnchorList.length - 1). You shouldn't create closure in a loop, and you can use a "factory" method to create closure instead.

OnMouseOver event and storing variable values

I am generating an HTML table dynamically using JavaScript. One of the table's columns contains an onmouseover event, and when the mouse cursor is over a particular cell, I wish to display cell specific information.
In my case, I wish to display the value of nPos via an alert() call as it was during the table construction.
Simplified example of my table creation:
for (var iLoop = 0 ; iLoop < nHitsPerPage ; iLoop++) {
var nPos = (nCurrentPageParam * SearchResults.nMaximumHitsPage) + iLoop;
//...
var cellInfo = tableResultsRow.insertCell(6);
//...
cellInfo.className = "noWrapCell";
cellInfo.innerHTML = "?";
cellInfo.style.cursor = "pointer";
// The line below is important - I need to store nPos value during the table consturction
cellInfo.onmouseover = function(){alert("nPos = " + nPos)};
cellInfo.onmousemove = function(){FileResort.LightboxShortInfo.update(event)};
cellInfo.onmouseout = function(){FileResort.LightboxShortInfo.hide(event)};
}
As you can see from the example above, I iteratively created a table within my for() loop. I want to store the value of the nPos within every row (record) which is different for each row (record).
My problem is that once I mouseover that particular cell, I get the same nPos value for every row (record), and that nPos is the current value of nPos at the particular application state.
I cannot find a way to record the nPos value as it was during the for() execution, which is important for me in identifying what record is stored in the particular row.
Is there a way to capture, or store the value of nPos for every row (record) during the table's initial construction?
You are yet another victim of the closure monster :)
Look at this:
cellInfo.onmouseover = function(){alert("nPos = " + nPos)};
the function here - let's call it lambda - will have to find the value of the variable nPos. Since nPos is not declared inside lambda, it will look for it in the upper level function, that is the function where nPos is created.
When the mouseover event will fire, the code that declares and sets nPos will already have run to completion, so nPos will have the value nHitsPerPage.
That is exactly what lambda will display. Unfortunately, that's not what you want :).
To get over that, you need to create a (lexical) closure, i.e. provide lambda with a value of nPos that suits your needs.
The way of doing it in Javascript is as follows:
cellInfo.onmouseover = function(p){
return function () { alert("nPos = " + p)};
} (nPos);
let's call nu the new function (the one that takes p as a parameter).
We changed lambda so that it now refers to p instead of nPos.
It means that when the mouseover event will fire, lambda will look for p in its upper function, which is now nu. And it will indeed find p there, as the parameter of nu.
p is a function parameter, so it recieves a copy of nPos at the time it is called.
It means lambda will refer to a different instance of nu's calling context for each instance of cellInfo.
Each instance of the mouseover event handler will now hold a copy of p that is set to the desired value of nPos.
Quick Psuedo Code:
var nPosArray = [];
var itemsArray = [];
for (var iLoop = 0 ; iLoop < nHitsPerPage ; iLoop++)
{
var nPos = (nCurrentPageParam * SearchResults.nMaximumHitsPage) + iLoop;
//...
var cellInfo = tableResultsRow.insertCell(6);
//...
cellInfo.className = "noWrapCell";
cellInfo.innerHTML = "?";
cellInfo.style.cursor = "pointer";
// The line below is important - I need to store nPos value during the table consturction
nPosArray.push(nPos);
cellInfo.addEventListener('mouseout', cellMouseOut(event));
cellInfo.onmousemove = function(){FileResort.LightboxShortInfo.update(event)};
cellInfo.onmouseout = function(){FileResort.LightboxShortInfo.hide(event)};
itemsArray.push(cellInfo);
}
function cellMouseOut(e)
{
for(iLoop = 0 ; iLoop < cellInfo.length; iLoop++)
{
if(e.target.id == cellInfo[iLoop].id)
{
alert('NPos: ' + nPosArray[iLoop]);
}
}
}

Alert of vars returns "length" or "item"

I have a 16x16 table, and I asign a lambda function (to pass parameters to the actually functional function) to all the td like that:
function handlerAsignment()
{
var trs = document.getElementsByTagName("tr");
var tds;
for (tr in trs) {
tds = trs[tr].getElementsByTagName("td");
for (td in tds){
tds[td].onclick = function() {
atack(tr, td);
};
}
}
}
As you will see, I have to pass the tr number and the td number regarding to the proper tr.
But then I have this function:
function atack(tr, td)
{
alert("Tr: " +tr+ " td: " +td);
}
And this show me the message "Tr: item td: length". Why???
Advice: I don't want to use event in the function atack to detect the correct td. I need this parameters to acces and work properly with a 16x16 multidimensional array (equal to the table, but with more information).
This is the classic closure misunderstanding. The function (closure) you're assigning to onclick has an enduring reference to the tr and td variables, not a copy of them as of when it was created. So all of the onclick functions use the same variables, with the same values; the values as they are at the end of both loops.
The usual answer here is to use a builder function so they use separate variables:
function handlerAsignment()
{
var trs = document.getElementsByTagName("tr");
var tds;
for (tr in trs) {
tds = trs[tr].getElementsByTagName("td");
for (td in tds){
tds[td].onclick = buildHandler(tr, td);
}
}
}
function buildHandler(tr, td) {
return function() {
atack(tr, td);
};
}
Now, the handler closes over the tr and td arguments passed into buildHandler, not the tr and td variables used in the loop. Since those arguments never change (each call to buildHandler gets its own), it solves the problem.
More on my blog: Closures are not complicated
Two other things worth mentioning:
You're not declaring td or tr anywhere, so you're falling prey to The Horror of Implicit Globals
You don't use for-in to loop through NodeList instances. for-in loops through the property names of objects. Using it with host-provided objects like NodeList is not guaranteed to work, and moreover relies on the host-provided object making some things enumerable and other things non-enumerable. Use a standard loop instead. So:
function handlerAsignment()
{
var trs = document.getElementsByTagName("tr");
var tds;
var trIndex, tdIndex;
for (trIndex = 0; trIndex < trs.length; ++trIndex) {
tds = trs[trIndex].getElementsByTagName("td");
for (tdIndex = 0; tdIndex < tds.length; ++tdIndex){
tds[tdIndex].onclick = buildHandler(trIndex, tdIndex);
}
}
}
function buildHandler(tr, td) {
return function() {
atack(tr, td);
};
}
More: Myths and realities of for..in
Live Example | Source
Finally, if you can rely on having the Array#forEach function from ECMAScript5 (e.g., you know your target browsers have it, or you're using a shim for it), the code can be simpler because forEach gives you the equivalent of buildHandler (a set of arguments to close over) for free:
function handlerAsignment()
{
var trs = document.getElementsByTagName("tr");
var tds;
var forEach = Array.prototype.forEach;
forEach.call(trs, function(tr, trIndex) {
forEach.call(tr.getElementsByTagName("td"), function(td, tdIndex) {
td.onclick = function() {
atack(trIndex, tdIndex);
};
});
});
}
Live Example | Source
You can safely use Array#forEach on any array-like object, not just arrays. NodeList objects are array-like.
for (tr in trs) isn't the thing you want. It sets tr to the name of each property of trs in turn. Since trs is a NodeList, its named properties are length and its item method.
The for (tr of trs) syntax would work, but you're coding for the browser, and browser support for that isn't widespread.
A NodeList isn't a real array, so you can't easily use Array.forEach on it.
So, your best bet is using for (var i = 0 ; i < trs.length ; i++) plus trs[i] plus T.J.'s buildHandler trick to make sure that the variable your handler closes over doesn't change after the closure is created.
No one has discussed the elephant in the room: creating a handler that passes a reference to the element it's on is unnecessary since the handler can be set up to reference that element as this by default.
Similarly, passing a reference to that element's parent also makes no sense when that element is available as this.parentNode.
A simple function to add listeners using either addEventListener or attachEvent as appropriate:
function addListener(element, event, fn) {
// Use addEventListener if available
if (element.addEventListener) {
element.addEventListener(event, fn, false);
// Otherwise use attachEvent, set this and event
} else if (element.attachEvent) {
element.attachEvent('on' + event, (function (el) {
return function() {
fn.call(el, window.event);
};
}(element)));
// Break closure and primary circular reference to element
element = null;
}
}
Use the above to attach the listeners on the cells, then within the atack (attack?) function:
function atack(evt) {
var cell = this;
var row = cell.parentNode;
alert("Tr: " + row + " td: " + cell);
}
If you investigate event delegation you may find you only need a single listener on the table.

two ways of looping through a JavaScript array of elements with different outcomes

I have a question about looping in JavaScript. Mostly I use jQuery but now I've decided to make an easy game in pure JavaScript.
if I loop through all my 'td' with this method it works, The cells[i] are td elements and I can attach events to them.
for(i = 0; i < cells.length; i++){
if(cells[i].nodeName == 'TD')
{
cells[i].onclick = function(){
// call funciton on onclick
};
}
}
But if I do like this, each element are just index numbers and the two at the end are length and item.
for(var cell in cells){
// cell is a number
}
What's the difference and why doesn't the foreach-loop work like I want it to?
cell is a number because it is an index to the cells HTMLCollection. In the second loop you'd use the values like so:
for(var cell in cells){
if(cells[cell].nodeName == 'TD')
{
cells[cell].onclick = function(){
GameTurnExecute(player);
};
}
}
You can also say:
for(var cell in cells){
if(cells.hasOwnProperty(cell))
{
cells[cell].onclick = function(){
GameTurnExecute(player);
};
}
}
not to see properties comming from prototype chain. (https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/hasOwnProperty)
In fact, almost everty time you use for in in javascript you want to use hasOwnProperty.

Categories