I have this click event attached to each button and when I click on each of them, it is printing the output meant for the third button. I'm not sure what is going on.
<!DOCTYPE html>
<html>
<head>
<title>JS Bin</title>
</head>
<body>
<button>test1</button>
<button>test2</button>
<button>test3</button>
</body>
<script>
var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
nodes[i].addEventListener('click', function() {
console.log('You clicked element #' + i);
});
}
</script>
</html>
when I click on any of the buttons, it is printing
"You clicked element #3"
Simple solution to this:
<!DOCTYPE html>
<html>
<head>
<title>JS Bin</title>
</head>
<body>
<button>test1</button>
<button>test2</button>
<button>test3</button>
</body>
<script>
var nodes = document.getElementsByTagName('button');
console.log(nodes);
for (var i = 0; i < nodes.length; i++) {
//converted click function into an IIFE so it executes then and there only
nodes[i].addEventListener('click', (function (j) {
return function () {
console.log('You clicked element #' + j);
}
})(i));
}
</script>
</html>
You should go through two concepts to understand this thing
1) Closures
2) Javascript is single-threaded and synchronous. So how does it handle events?
Here is what it is happening in your code:
==> for loop gets executed synchronously as it is part of javascript engine post which javascript handles event queue which is a FIFO (first in first out)
==> When for loop finished value of i is three which remains in memory until the function inside it executes
==> Each time it takes a value 3 and prints it.
When this button is listening to event, at that time the value of i is nodes.length -1 that is 2. Because loop has already finished it's execution and have set value of i to 2.
So it is consoling You clicked element #3.
Such issues arise because of scope & closure
Create an IIFE and pass the value of i.
Hope this snippet will be useful
var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
(function(i){
nodes[i].addEventListener('click', function() {
console.log('You clicked element #' + i);
});
}(i))
}
Check this jsfiddle
This is other way using jQuery.
$("button").each(function(e) {
$(this).data("number", e);
}).bind("click", function(e) {
console.log("You clicked element #" + $(this).data("number"));
});
https://jsfiddle.net/ChaHyukIm/uxsqu70t/3/
This is the closure with function inside a loop issue.
JavaScript closure inside loops – simple practical example
Watch out for this!
Side note: questions around this issue are frequently asked in interviews to demonstrate proficiency with JS.
Related
I am total beginner (1 week learning this ...and already thinking about quitting before getting crazy :D) and this is my first question, so please, if I do something wrong, just let me know.
I am trying to solve a small Javascript exercise about adding a list to a HTML file with a JS function.
I have created this function, but It is not working. I would say the problem is that I don't know how to indicate the bands name variable inside the ".createTextNode()" .
This is the function I have in a JS file :
function addBands() {
for (i = 0, i < 0, i++) {
var banda = document.createElement("LI");
var nombre= document.createTextNode([0]);
banda.appendChild(nombre);
document.getElementById("band-list").appendChild(banda)
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1> MY FAVORITE BANDS</h1>
<ul id="band-list">
</ul>
<script src="exercisesJS.js"></script>
<script>addBands(['Dire Straits', 'Kansas', 'Steely Dan'])</script>
</body>
</html>
The output should be a list with the name of the 3 bands in the Function, or any other bands (could be 3, or 6 ...etc...
Always check the error console. It's currently telling you that
for (i = 0, i < 0, i++) {
is a syntax error. You mean:
for (i = 0; i < 0; i++) {
That fixes the syntax. However that's still logically wrong, since it means your loop will never run (since the iterator variable i starts at 0, and is told to run while it is under 0, a condition it fails right from the beginning.)
Looking at your code, there's other problems. You're passing an array of band names to the function, but the function isn't set up to receive it. So, we need:
function addBands(bands) {
That means the inner part of the function can access what was passed to it. It also means we can base our loop on the number of bands that were passed, and use the iterator band as the textual output.
function addBands(bands) { //<-- receive incoming array
for (i = 0; i < bands.length; i++) { //iterate bands.length times
var banda = document.createElement("LI");
var nombre= document.createTextNode(bands[i]); //output iterator band name
banda.appendChild(nombre);
document.getElementById("band-list").appendChild(banda)
}
}
While we're here, there's a couple of other optimisations we can make. Firstly, there's no sense in freshly looking up the ul element each time the loop runs. So let's cache it outside the loop. Secondly, while createTextNode() is fine, you may be interested to know that it's easier to just use textContent on the parent node. Putting it all together:
function addBands(bands) { //<-- receive incoming array
let ul = document.getElementById("band-list"); //cache UL
for (i = 0; i < bands.length; i++) {
var banda = document.createElement("LI");
banda.textContent = bands[i];
ul.appendChild(banda)
}
}
Refer to #Utkanos's answer to exactly understand what went wrong in your code. Given that, I suggest the following solution, which uses ES2015 .forEach method of arrays, for looping over your provided list.
function addBands(liArray) {
liArray.forEach(liText => {
const li = document.createElement('li')
const liTextNode = document.createTextNode(liText)
li.appendChild(liTextNode)
document.getElementById("band-list").appendChild(li)
})
}
addBands(['Dire Straits', 'Kansas', 'Steely Dan'])
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1> MY FAVORITE BANDS</h1>
<ul id="band-list">
</ul>
<script></script>
</body>
</html>
If I write the html:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<h1 id="message">
</h1>
and the JS:
messages = ["Here", "are", "some", "messages."]
$(function() {
for (var i = 0; i < messages.length; i++) {
$('#message').html(messages[i]).delay(1000);
}
});
and load the page, I expect to see each string in the array show up with a delay in between. However, all I see is "messages." appear. It seems that the for loop iterates immediately through each value in the array before performing any delay.
I have seen another method for getting the desired visual result (How can I change text after time using jQuery?), but I would like to know why the earlier method does not work. What is going on when this code is executed?
This is how i would delay my message changing.
function delayLoop(delay, messages) {
var time = 100;
$(messages).each(function(k, $this) {
setTimeout(function()
{
$("#message").html($this);
}, time)
time += delay;
});
}
delayLoop(1000, ["Here", "are", "some", "messages."]);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="message">
</div>
All I did was for each message delay by an additional delay time.
It works in async mode so its not ui blocking and the messages will display a second after one another.
EDIT:
Removed the .delay from the .html it is redundant.
Note that jQuery's delay is specifically for effects; methods like html do not use the effects queue and are therefore not affected by delay.
This is a problem better solved with JavaScript's native setTimeout function. There are many ways to do this; in fact, you don't even need jQuery!
let messages = ["Here", "are", "some", "messages."];
let delay = 1000;
let header = document.getElementById("message");
messages.forEach(function(message, i) {
setTimeout(function() {
header.innerText = message;
}, delay * i);
});
<h1 id="message" />
You would need something along the lines of
$(function() {
for (var i = 0; i < messages.length) {
var done=false;
$('#message').html(messages[i]).delay(1000).queue(function(){
done=true;
$(this).dequeue();
});
if(done==true){
i++;
}
}
});
Thank you for the answers and comments--very helpful.
I also found this post helpful: Node.js synchronous loop, and from it wrote this (which also works):
function changeText() {
var msg = messages.shift();
$('#message').html(msg).show(0).delay(1000).hide(0, function() {
if (messages.length > 0) {
changeText();
}
});
}
(I used .show and .hide because without them only one of the array values appeared. I'm not sure why that is, but that's a question for another time.)
I'm writing a simple chrome extension that lists all the open tabs, I have this code on it
function changeTab(tabID){
chrome.tabs.update(tabID,{active:false})
}
chrome.windows.getCurrent({populate: true},function (window){
list = document.getElementById('open-tabs');
for (var i = 0; i < window.tabs.length; i++)
{
var li = document.createElement('li');
var element = document.createElement('a');
element.setAttribute('href','#');
element.innerHTML = window.tabs[i].title;
element.addEventListener("click",function(){
changeTab(window.tabs[i].id);
},false);
li.appendChild(element);
list.appendChild(li);
}
});
It lists the open tabs, but doesn't seem to add the onClick event, when I checked the chrome console I get this
Why is not adding the event correctly?
--edit--
Adding the html if it helps
<!doctype html>
<html>
<head>
<title>Count Me</title>
<link rel="stylesheet" href="popup.css" type="text/css">
<script src="popup.js"></script>
</head>
<body>
<div id="main">
<ul id="open-tabs"></ul>
</div>
</body>
</html>
Edit2
I tried using the sugestion given on an answer using the .bind(this,i) but still doesn't work, I added console.log() to see what's happening, and it seems it's not going inside the addEventListener heres the code with the log calls:
function changeTab(tabID){
chrome.tabs.update(tabID,{active:false})
}
chrome.windows.getCurrent({populate: true},function (window){
list = document.getElementById('open-tabs');
for (var i = 0; i < window.tabs.length; i++)
{
var li = document.createElement('li');
var element = document.createElement('a');
element.setAttribute('href','#');
element.innerHTML = window.tabs[i].title;
console.log('before');
console.log(window.tabs[i].id);
element.addEventListener("click",function(iVal){
console.log('inside');
changeTab(window.tabs[iVal].id);
}.bind(this,i),false);
console.log('after');
console.log(window.tabs[i].id);
li.appendChild(element);
list.appendChild(li);
}
});
As you can see I have a Before and After console.log() as well as inside the addEventListener and it doesn't seem to call anything inside the addEventListener as you can see here:
It's calling the console.log inside the addEventListener but still isn't working
Try adding a closure around the function
(function(num) {
element.addEventListener("click",function(){
changeTab(window.tabs[num].id);
},false);
})(i)
The event will be executed a later stage when you click the element. So when the for loop is completed, i always points to last iterated value.
So enclosing it in an anonymous function creates a closure around the variable which will be available at the time the click event occurs.
Your function callback is happening in a context that does not recognize i..
You can bind the i value to function and by that make it work:
element.addEventListener("click",function(iVal){
changeTab(window.tabs[iVal].id);
}.bind(this,i),false);
Really a newbie question but I can't seem to find the answer. I need to have this html file show a bunch of random numbers, separated by 1 second intervals. For some reason (maybe obvious) it is only showing me the last one unless I have 1 alert after each random number generated. How can I correct this?
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var randomnumber
var message
function placePossibleWinner()
{
randomnumber=Math.floor(Math.random()*11);
message="Teste ";
message=message.concat(randomnumber.toString());
document.getElementById("WINNER").innerHTML=message;
//alert(".")
}
</script>
</head>
<body>
<script type="text/javascript">
function runDraw()
{
var i=1
alert("start")
while (i<20)
{
setTimeout("placePossibleWinner()",1000)
i++
}
}
</script>
<h1>H Draw</h1>
<p id="WINNER">Draw</p>
<p></p>
<button onclick="runDraw()">Get me winner!</button>
</body>
</html>
Thanks in advance for any answers/comments.
The problem is all your setTimeouts are being triggered at the same time. Adding alerts pauses the JavaScript execution, so you see each number. Without that, after 1 second, all 19 setTimeouts run (one after another) and you just see one number (the screen is updated so fast, you just see one).
Try using setInterval instead.
function runDraw() {
var i = 1;
var interval = setInterval(function(){
if(i < 20){
placePossibleWinner();
i++;
}
else{
clearInterval(interval);
}
}, 1000);
}
This will run the function once every second, until i is 20, then it will clear the interval.
I believe you want setInterval instead. using setTimeout in a loop will just queue up 20 calls immediately and they will all fire at once 1 second later. Also, you are setting the innerHTML of the p which will overwrite any previous text.
function placePossibleWinner() {
// add a var here, implicit global
var randomnumber=Math.floor(Math.random()*11);
// add a var here, implicit global
message="Teste " + randomnumber + '\n'; // new line
document.getElementById("WINNER").innerHTML += message; // concat, don't assign
}
function runDraw() {
var counter = 1;
var intervalID = setInterval(function () {
if (counter < 20) {
placePossibleWinner();
counter++;
} else {
clearInterval(intervalID);
}
}, 1000);
}
You are resetting your message in your functions and you are calling placePossibleWinner() the wrong way... you want to use setInterval. Below is a modification of your html/javascript
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var randomnumber;
var message = "Teste ";
var timesCalled = 0;
var funtionPointer;
function placePossibleWinner()
{
timesCalled++;
randomnumber=Math.floor(Math.random()*11);
message=message.concat(randomnumber.toString());
document.getElementById("WINNER").innerHTML=message;
if (timesCalled > 20)
{
clearInterval(functionPointer);
}
}
</script>
</head>
<body>
<script type="text/javascript">
function runDraw()
{
var i=1
alert("start")
functionPointer = setInterval(placePossibleWinner,1000)
}
</script>
<h1>H Draw</h1>
<p id="WINNER">Draw</p>
<p></p>
<button onclick="runDraw()">Get me winner!</button>
</body>
</html>
To start with,
setTimeout("placePossibleWinner()",1000)
should be
setTimeout(placePossibleWinner,1000)
The parameter to setTimeput should be a reference to a function. See JavaScript,setTimeout
I am trying to make each number displayed clickable. "1" should alert() 80, "2" should produce 60, etc.
However, when the alert(adjust) is called, it only shows 0, not the correct numbers. However, if the commented out alert(adjust) is uncommented, it produces the correct number on page load, but not on clicking.
I was wondering why the code inside addEvents cannot access the previously defined variable adjust.
<html>
<head>
<script type="text/javascript" charset="utf-8" src="mootools.js"></script>
<script type="text/javascript" charset="utf-8">
window.addEvent('domready', function() {
var id_numbers = [1,2,3,4,5];
for(var i = 0; i<id_numbers.length; i++) {
var adjust = (20 * (5 - id_numbers[i]));
// alert(adjust);
$('i_' + id_numbers[i]).addEvents({
'click': function() {
alert(adjust);
}
});
}
});
</script>
</head>
<body>
<div id="i_1">1</div>
<div id="i_2">2</div>
<div id="i_3">3</div>
<div id="i_4">4</div>
<div id="i_5">5</div>
</body>
</html>
Thanks.
You are having a very common closure problem in that for loop.
Variables enclosed in a closure share the same single environment, so by the time the click callbacks are called, the for loop would have run its course, and the adjust variable will be left pointing to the last value it was assigned.
You can solve this with even more closures, using a function factory:
function makeClickHandler(adjust) {
return function() {
alert(adjust);
};
}
// ...
for(var i = 0; i<id_numbers.length; i++) {
var adjust = (20 * (5 - id_numbers[i]));
$('i_' + id_numbers[i]).addEvents({
'click': makeClickHandler(adjust)
});
}
This can be quite a tricky topic, if you are not familiar with how closures work. You may to check out the following Mozilla article for a brief introduction:
Mozilla Dev Center: Working with Closures