I'm trying to create buttons dynamically with unique listeners and handlers by using a for loop, but unfortunately I must be doing something wrong because only the last button works.
Even more surprising is the fact that when clicking the last button instead of "Button No.3" it returns "Button No.4"
Bellow is the code and here is a jsfiddle http://jsfiddle.net/y69JC/4/
HTML:
<body>
<div id="ui">
some text ...
</div>
</body>
Javascript:
var uiDiv = document.getElementById('ui');
uiDiv.innerHTML = uiDiv.innerHTML + '<br>';
var results = ["Button one","Button two","Button three","Button four"];
for(var n=0;n<results.length;n++)
{
uiDiv.innerHTML = uiDiv.innerHTML + '<button id="connect'+n+'">option'+n+':'+results[n]+'</button><br>';
var tempId = document.getElementById('connect'+n);
tempId.addEventListener('click', function(){console.log("Button No."+n)}, false);
}
Thanks!
It's a classic case when you need a closure: Change to:
var uiDiv = document.getElementById('ui');
uiDiv.innerHTML = uiDiv.innerHTML + '<br>';
var results = ["Button one", "Button two", "Button three", "Button four"];
for (var n = 0; n < results.length; n++) {
// Note: do not use .innerHTML. Create new element programatically
// and then use .appendChild() to add it to the parent.
var button = document.createElement('button');
button.id = 'connect' + n;
button.textContent = 'option' + n + ':' + results[n];
uiDiv.appendChild(button);
button.addEventListener('click', /* this is an inline function */ (function (n2) {
// Here we'll use n2, which is equal to the current n value.
// Value of n2 will not change for this block, because functions
// in JS do create new scope.
// Return the actual 'click' handler.
return function () {
console.log("Button No." + n2)
};
})(n)); // Invoke it immediately with the current n value
}
The reason for this is that a for loop does not create a scope, so "Button No. " + n was always evaluated with the n equal to the number of elements in results array.
What has to be done is to create an inline function accepting n as a parameter and call it immediately with the current value of n. This function will then return the actual handler for the click event. See this answer for a nice and simple example.
Edit: Your code is using innerHTML property to add new buttons in a loop. It is broken because every time you assign using uiDiv.innerHTML = ..., you are deleting all contents present previously in this div and re-creating them from scratch. This caused ripping off all event handlers previously attached. Schematically, it looked like this:
uiDiv.innerHTML = ""
// First iteration of for-loop
uiDiv.innerHTML === <button1 onclick="...">
// Second iteration of for-loop
// 'onclick' handler from button1 disappeared
uiDiv.innerHTML === <button1> <button2 onclick="...">
// Third iteration of for-loop
// 'onclick' handler from button2 disappeared
uiDiv.innerHTML === <button1> <button2> <button3 onclick="...">
you can write
onclick = 'myFunction(this.id):'
in the button then later:
function myFunction(idNum){console.log("Button No."+idNum);}
also here are some improvements you may want to implement:
uiDiv.innerHTML = uiDiv.innerHTML + ....;
uiDiv.innerHTML += ....;
you also want to declare "uiDivContent" before the loop then afterward write:
uiDiv.innerHTML = uiDivContent;
The event handler is bound to n, which has the value results.length by the time the event fires.
You have to close the value of n by making a copy, you can do this by calling a function. This construction is known as a closure.
for(var n=0;n<results.length;n++)
{
uiDiv.innerHTML = uiDiv.innerHTML + '<button id="connect'+n+'">option'+n+':'+results[n]+'</button><br>';
var tempId = document.getElementById('connect'+n);
tempId.addEventListener('click', (function(bound_n) {
console.log("Button No."+ bound_n);
})(n)), false); // <-- immediate invocation
}
If n were an object, this wouldn't work because objects (unlike scalars) get passed-by-reference.
A great way to avoid having to write closures, in all your for-loops, is to not use for-loops but a map function with a callback. In that case your callback is your closure so you get the expected behaviour for free:
function array_map(array, func) {
var target = [], index, clone, len;
clone = array.concat([]); // avoid issues with delete while iterate
len = clone.length;
for (index = 0; index < len; index += 1) {
target.push(func(clone[index], index));
}
return target;
}
array_map(results, function (value, n) {
uiDiv.innerHTML = uiDiv.innerHTML
+ '<button id="connect'+n+'">option'+n+':'+results[n]+'</button><br>';
var tempId = document.getElementById('connect'+n);
tempId.addEventListener('click', function(){console.log("Button No."+n)}, false);
});
Related
I was trying to display members' names in the div element after 3 seconds by using for loop with setTimeout() function. But I'm getting an undefined values of the names array. And even the value of var i displaying 4. I don't know why. Can anyone explain to me why and how to fix that.?
Thanks in advance!
JS Code:
function members() {
var arr = ["Joseph","John","Kumar","Shiva"];
var i;
for(i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log("Member = " + i + ". " + arr[i]);
document.getElementById("Names").innerHTML = "Member = " + i + ". " + arr[i];
}, 3000);
}
}
members();
<div id="Names"></div>
First, Because var is a function scope ( not block scope like for loop ), when the setTimeout execute after 3 sec, the loop will be ended and the value of i will be equal to the arr.length, and arr[arr.lenght] will equal to undefined.
To solve this issue, one solution is using let instead of var.
Second, If you want to add the members to HTML, you need to use += to concatenate with the previous string not = which will re-initialize the innerHTML
const names = document.getElementById("Names");
function members() {
var arr = ["Joseph","John","Kumar","Shiva"];
for(let i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log("Member = " + i + ". " + arr[i]);
document.getElementById("Names").innerHTML += `Member = ${(i + 1)}. ${arr[i]}<br />`;
}, 3000);
}
}
members();
<div id="Names"></div>
I think better solution for you here is to execute a function after 3 seconds, rather then putting the timeout inside for loop.
That was what was causing your issue, as the var is a function scoped variable. So by putting timeout inside of for loop, the last element would be undefined since your script would read the last element as arr.length, which is 4, and you don't have arr[4], hence it is undefined.
Also, your innerHTML was not written properly, if you want to print multiple values, you need to use innerHTML += instead of innerHTML = because innerHTML = will always print the last value from your loop, because it erases the innerHTML of your element in each new iteration.
I modified your code snippet below.
https://codepen.io/Juka99/pen/VwxpXwL
I am dynamically creating a table of elements and storing them in an array. The following may seem like an absolute nightmare but this is how I have decided to sort it. My problem now comes to the addEventListener where I want to add an onclick event connected to PlayMusic(). I have tried a simple .onclick = and left out the function(){} but then the PlayMusic() gets executed immediately. Having the function(){} in there, when I click on one of these elements the first param (i) is the "last number used" (aka 22 out of 21 elements). How would I go about making sure each of these onclicks has the correct index in their params?
var thetable = document.getElementById("mustable");
for(var i=0; i<fullists.length-1; i++)
{
fullists[i][2] = [];
fullists[i][3] = [];
for(var j=0; j<fullists[i][1].length; j++)
{
var row = thetable.insertRow();
fullists[i][2][j] = row.insertCell();
fullists[i][2][j].className = "musentry";
var header = fullists[i][0].substring(0,fullists[i][0].lastIndexOf("."));
if(fullists[i][1][j][1] != undefined)
var title = fullists[i][1][j][1];
else
var title = fullists[i][1][j][0].substring(fullists[i][1][j][0].lastIndexOf("/"));
fullists[i][2][j].innerHTML = header + "<br /><b>" + title + "</b>";
fullists[i][2][j].addEventListener("click",function() { PlayMusic(i,j); },false);
fullists[i][3][j] = 0;
}
}
The issue is that by the time the function executes, i already has a different value because the loop already continued executing. If you change your loop to use let i instead of var i (same for j) it will work, because let in the for iterator variable has a special behavior where it actually creates another copy of the variable scoped to the inside of the loop on every iteration, so that copy won't change.
Another way, which is basically the same thing but done explicitly: Store it inside another block-scoped variable first. E.g. const i2 = i and then use i2 inside the function () {}. Same for j.
Alternatively, write .addEventListener(..., PlayMusic.bind(null, i, j)). With bind you can create a new function from a function, where a this and arguments are already bound to it. Since the binding happens immediately and thereby captures the current values of i and j, that solves it too.
I'm working with a list of images that may change in number, so fixed IDs and event listeners are not practical. The below code produces the correct number of buttons with the correct IDs, but only the last one has a functional event listener.
for (var i = 0; i < amount; i++) {
!function(index) {
if (items[index].classList.contains('current')) {
document.getElementById('selectButtons').innerHTML += '<button id=\"bitems' + index + '\"> ⬤ <span class=\"offscreen\">Item ' + i + '</span></button>';
}
else
{
document.getElementById('selectButtons').innerHTML += '<button id=\"bitems' + index + '\"> ◯ <span class=\"offscreen\">Item ' + i + '</span></button>';
}
document.getElementById('bitems' + index).addEventListener("click", function(ev) {
alert("clicked");
});
}(i);
}
Apparently the IIFE is not storing the individual variables like it is supposed to, but I can't figure out why. After all, that is the entire purpose of an IIFE within a loop.
Any help would be greatly appreciated.
IIFE is working fine. Actually every time you update the innerHTML for selectButtons, the DOM is recreated, and all the events attached to it are gone!
Instead of updating the innerHTML in each iteration, you can append the buttons to it instead like:
for (var i = 0; i < 10; i++) {
!function(index) {
var button = document.createElement("BUTTON");
var t = document.createTextNode("Button" + index);
button.appendChild(t);
document.getElementById('selectButtons').appendChild(button);
button.addEventListener("click", function(ev) {
alert("clicked +" +index);
});
}(i);
}
Please do add the conditions around it that you need.
Every time you do innerHTML += you are replacing the entire HTML, which removes any previously installed event handlers. This is one perfectly good reason not to treat HTML as a bunch of strings that you innerHTML onto the page. Instead of strings, think in terms of elements, as in another answer. Then you also don't need to use IDs as a poor man's "variable name" to reference elements; you can just use the element itself.
You don't need a clumsy IIFE. That's what let is for.
Here's a cleaned-up version of your code:
var buttons = document.getElementById('selectButtons');
for (let i = 0; i < amount; i++) {
var current = items[i].classList.contains('current');
var button = document.createElement("BUTTON");
var bullet = document.createTextNode(current ? '◯' : '⬤')
var span = document.createElement('span');
van spanText = `Item ${i}`;
span.className = 'offscreen';
span.appendChild(spanText);
button.appendChild(bullet);
button.appendChild(span);
buttons.appendChild(button);
button.addEventListener('click', () => alert(`clicked ${i}`));
}
If you want to save a line or two, you could take advantage of the fact that appendChild returns the appended child, and chain:
buttons.appendChild(button).appendChild(span).appendChild(spanText);
If you're going to be doing a lot of this, it would be best to create some tiny utility routines:
function createElementWithText(tag, text) {
var b = document.createElement(tag);
var t = document.createTextNode(text);
b.appendChild(t);
return b;
}
function button(text) { return createElementWithText('button', text); }
function span(text) { return createElementWithText('span', text); }
Now you can write your code more concisely as:
var buttons = document.getElementById('selectButtons');
for (let i = 0; i < amount; i++) {
var current = items[i].classList.contains('current');
var button = button(current ? '◯' : '⬤');
var span = span(`Item ${i}`);
span.className = 'offscreen';
buttons.appendChild(button).appendChild(span);
button.addEventListener('click', () => alert(`clicked ${i}`));
}
Actually, it would moderately preferable to create a document fragment, add all the buttons to it in advance, then insert it into the DOM a single time.
However, in practice, you would be better off using some kind of templating language, in which you could write something like:
<div id="selectButtons">
{{for i upto amount}}
<button {{listen 'click' clicked}}>
{{if items[i] hasClass 'current'}}◯{{else}}⬤{{endIf}}
<span class="offscreen">Index {{i}}</span>
</button>
{{endFor}}
</div>
It's beyond the scope of this answer to recommend a particular templating language. There are many good ones out there, such as Mustache, that google can help you find, with a search such as "javascript templating languages".
I'm trying to refactor my window.onload function so as to avoid redundancy. I'd like to loop over the elements I'm assigning to global variables, using their ids. Initially, I was able to assign onclick functions with a loop, but now I'm not able to reproduce this in a fiddle. But the main issue is simply trying to do this (see fiddle):
var gragh, gorgh;
var ids = ["gragh", "gorgh"];
for (var i = 0; i < ids.length; i++) {
ids[i] = document.getElementById(ids[i]);
// TypeError: document.getElementById(ids[i]).onclick = doStuff;
}
//console.log(gragh); undefined
This is supposed to assign the variables gragh and gorgh to p elements which have the same ids. Within the loop, ids[i] seems to refer to the p elements. After the loop, however, these variables are undefined. This also doesn't work when looping through an array with these variables not surrounded by quotes. I've even tried using eval(), with mixed results. So my question is, how can I get this to work? And also, why doesn't this work? If ids = [gragh, gorgh] (without the quotes), what do these variables within the array refer to?
Don't reassign it in your loop, try using a new array to populate. Think of it as a reference - you're modifying it while looping.
var gragh, gorgh;
var ids = ["gragh", "gorgh"];
var newSet = [];
for (var i = 0; i < ids.length; i++) {
newSet[i] = document.getElementById(ids[i]);
}
Loop will finish it's looping much before this onclick executes.So at that time the value of i will be the upper limit of the loop.
A work around of this a closure
var gragh;
var ids = ["gragh", "gorgh"];
for (var i=0; i<ids.length; i++) {
(function(i){ // creating closure
console.log(i)
document.getElementById(ids[i]).onclick = doStuff
})(i) // passing value of i
}
document.getElementById("gragh").innerHTML = "ids[0]: " + ids[0] + ", ids[1]: " + ids[1]
function doStuff() {
document.getElementById("gorgh").innerHTML = "ids[0]: " + ids[0] + ",ids[1]: " + ids[1] + ", var gragh: " + gragh;
}
gragh is undefined since you haveonly declared it but never initialized it
JSFIDDLE
var rows = document.getElementsByClassName('row');
for (var i = 0, l = rows.length; i < l; i++) {
if (i % 2 === 0) {
$(rows[i]).click(function () {
alert('I am line number ' + i);
}
}
}
Hi,
how would I get actual line number for each row ? since all I get when I trigger click event on an even row, upper bound value is alerted (i.e: rows.length = 7, i would be 6 for each row clicked).
The problem is that upon click event is triggered, the i variable was already changed by the loop iteration. Theoretically you can use closure to make things working, i.e.
for (var i = 0, l = rows.length; i < l; i++) {
if (i % 2 === 0) {
(function(i) {
$(rows[i]).click(function() {
alert("I am line number " + i);
});
)(i);
}
}
Practically, if you use jQuery (as I understood from the code), it is easier to use :even selector:
$(".row:even").click(function() {
alert("I am line number " + $(".row").index(this));
});
The reason you're getting the wrong number is that the event handler functions you're creating get an enduring reference to the i variable, not a copy of it as of when they're created.
The way to solve it is to have the handler close over something that won't change. Here are three ways to do that, the first is specific to jQuery (it looks like you're using jQuery):
jQuery's each
It looks like you're using jQuery, in which case you can use its each to get an index to use that won't change:
var rows = $(".row");
rows.each(function(index) {
if (index % 2 === 0) {
$(this).click(function() {
alert('I am line number ' + index);
});
}
});
Now, the event handler function closes over the index argument of the call to the function we give each, and since that index argument never changes, you see the right number in the alert.
Use a builder function
(Non-jQuery) You can solve this with a builder function:
var rows = document.getElementsByClassName('row');
for (var i = 0, l = rows.length; i < l; i++) {
if (i % 2 === 0) {
$(rows[i]).click(buildHandler(i));
}
}
function buildHandler(index) {
return function () {
alert('I am line number ' + index);
};
}
Here, the event handler function closes over the index argument in buildHandler, and since that index argument never changes, you see the right number in the alert.
forEach
(Non-jQuery) You can also use ES5's forEach function (which is one of the ES5 features you can shim on a pre-ES5 environment) to solve this:
var rows = document.getElementsByClassName('row');
Array.prototype.forEach.call(rows, function(row, index) {
if (index % 2 === 0) {
$(row).click(function () {
alert('I am line number ' + index);
});
}
});
This works the same way as the two above, by closing over index, which doesn't change.