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.
Related
I'm a first year student I've been scrolling through Stack Overflow and have read a lot about the object problem (reference) but I can't figure out the solution to my problem.
I have made arrays of objects and looping over them to fill a div with all the info like img, name, value, so far no problem here.
The problem is with filling a mouseover function (attached to the image) with the object I'm looping through at the moment, so later when I hover over the image all the info of that particular object is shown on another div.
for (i = 0; i < arrgezelschap.lenght; i++) {
var x = arrgezelschap[i];
var element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
element.addEventListener('mouseover', function() {
showinfo(x)
});
inhoud.append(element);
}
In the function showinfo(object) the output is always the last object of the array.
Why is this and what do I need to do so that it saves or points to the object that it's looping through at the moment in my function?
TL;DR: change var x to let x
I can't really do a better job explaining than Creating closures in loops: A common mistake, but I'll take a shot at rephrasing it.
Compare the output of these two snippets (below). The only difference is var vs let. The example demonstrates creating 5 functions in a loop, but does not call them yet. Each function references variables declared inside the loop, outside the loop and in the for itself. Then, at the end of the loop, we call all the functions to see what we got.
In the first case, the variables outside, i (the loop variable) and inside (declared inside the loop) are all declared with var. They are the same variable on every iteration of the loop. The inside var is hoisted to the top of the scope (outside the loop).
When we call all the functions we created, we will see that they all refer to the one-and-only instance of each variable, and they all have the value that the variables have after completion of the loop.
let functions = [];
var outside = 0;
for (var i = 0; i < 5; ++i) {
outside = i * 10;
var inside = i * 100;
functions.push(() => { console.log(outside, i, inside); })
}
functions.map(f => f()); // call all the functions
Output:
40 5 400
40 5 400
40 5 400
40 5 400
40 5 400
In this second example, the variables are all declared with let. The variable i declared in the for and the variable inside declared inside the body of the loop are different variables on each iteration of the loop. But the outside variable is declared outside the loop, so there's still only one outside variable that is used in every iteration of the loop.
When we call all the functions we made this time, we see that each function is displaying a different variable i and inside and their values are the value they held during that particular iteration of the loop, because the variables only existed for that iteration of the loop and the function was bound to the instance of the variable that was used for that iteration. But the outside variable is the same variable every iteration and holds only one value: the value that it has at the end of the loop.
let functions = [];
let outside = 0;
for (let i = 0; i < 5; ++i) {
outside = i * 10;
let inside = i * 100;
functions.push(() => { console.log(outside, i, inside); })
}
functions.map(f => f()); // call all the functions
Output:
40 0 0
40 1 100
40 2 200
40 3 300
40 4 400
In your case, each function binds to the same (one and only) variable x. If you change your declaration of x from var x to let x then you will get a different variable x for each iteration of the loop, and the event listener function will be bound to a different x each time, which will have the value corresponding to that iteration of the loop.
Footnote: Hopefully functions.map(f => f()); is not confusing for you. It just calls all the functions in the array. It is the same as this:
for (var index = 0; index < functions.length; ++index) {
functions[index]();
}
This is because x is a reference here, not a value and it change while you loop. Have a look at this :
let x = 0;
let fcn = a => console.log(a);
function execAnotherFcn(fcn) {
fcn(x);
}
execAnotherFcn(fcn);
x++;
execAnotherFcn(fcn);
You could use the dataset attribute to store your information.
Here's my implementation:
const root = document.querySelector('#root');
function createImagePlaceholder(color, data) {
const el = document.createElement('div');
el.style.width = '50px';
el.style.height = '50px';
el.style.margin = '5px';
el.style.backgroundColor = color;
el.dataset = data;
root.appendChild(el);
el.addEventListener('mouseover', () => {
document.querySelector('pre').innerText = JSON.stringify(data);
});
el.addEventListener('mouseleave', () => {
document.querySelector('pre').innerText = '';
});
}
createImagePlaceholder('red', { text: 'I am a red block' });
createImagePlaceholder('blue', { text: 'I am a blue block' });
<div id="root"></div>
<pre><pre>
You can fix this by making the scope of element block level.
This happens because here the value of x is send as a closure and the var is defined as function level.The event listner function will get executed at a future time(not to the main thread), so at that time the value of x is changed by the loop to the last value.
This can be done using the let key word or using a IIFE.
1.
for (i = 0; i < arrgezelschap.length; i++) {
let x = arrgezelschap[i];
let element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
element.addEventListener('mouseover', function() {
showinfo(x)
});
inhoud.append(element);
}
2.
for (i = 0; i < arrgezelschap.lenght; i++) {
var x = arrgezelschap[i];
var element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
(function(x){element.addEventListener('mouseover', function() {
showinfo(x)
});})(x);
inhoud.append(element);
}
#PopHips answer explains the theory of what is going wrong. so here is a working example with your code so you can follow it.
for(i =0;i<arrgezelschap.lenght;i++){
var x = arrgezelschap[i];
var element = document.createElement("img");
element.src = x.artikelFoto + "k.jpg";
element.dataset.identifyer = i;
element.addEventListener('mouseover', function(e) {
showinfo(arrgezelschap[e.target.dataset.identifyer])
});
inhoud.append(element);
}
So because we're using an event listener it will give the first param as an EventArgs object, this contains a property called target that is the HTMLElement effected. we can use the dataset (data-) system to save the identifier to the object's dataset so we can use it in the event handler.
Please note this answer should not be used as it is, there is some really bad practice in this answer, NEVER CREATE A FUNCTION INSIDE A LOOP in production code.
I'm trying to make a click handler that calls a function; and that function gets a string and basically slices the last character and adds it to the front, and each time you click again it should add the last letter to the front.
It seem so easy at first that I thought I could just do it using array methods.
function scrollString() {
var defaultString = "Learning to Code Javascript Rocks!";
var clickCount = 0;
if (clickCount === 0) {
var stringArray = defaultString.split("");
var lastChar = stringArray.pop();
stringArray.unshift(lastChar);
var newString = stringArray.join('');
clickCount++;
} else {
var newArray = newString.split("");
var newLastChar = newArray.pop();
newArray.unshift(newLastChar);
var newerString = newArray.join("");
clickCount++;
}
document.getElementById('Result').innerHTML = (clickCount === 1) ? newString : newerString;
}
$('#button').on('click', scrollString);
Right now it only works the first time I click, and developer tools says newArray is undefined; also the clickCount stops incrementing. I do not know if it's an issue of scope, or should I take a whole different approach to the problem?
Every time you click you are actually reseting the string. Check the scope!
var str = "Learning to Code Javascript Rocks!";
var button = document.getElementById("button");
var output = document.getElementById("output");
output.innerHTML = str;
button.addEventListener("click", function(e){
str = str.charAt(str.length - 1) + str.substring(0, str.length - 1);
output.innerHTML = str;
});
button{
display: block;
margin: 25px 0;
}
<button id="button">Click Me!</button>
<label id="output"></label>
It is, in fact, a scoping issue. Your counter in inside the function, so each time the function is called, it gets set to 0. If you want a counter that is outside of the scope, and actually keeps a proper count, you will need to abstract it from the function.
If you want to keep it simple, even just moving clickCount above the function should work.
I do not know if it's an issue of scope
Yes, it is an issue of scope, more than one actually.
How?
As pointed out by #thesublimeobject, the counter is inside the function and hence gets reinitialized every time a click event occurs.
Even if you put the counter outside the function, you will still face another scope issue. In the else part of the function, you are manipulation a variable (newString) you initialized inside the if snippet. Since, the if snippet didn't run this time, it will throw the error undefined. (again a scope issue)
A fine approach would be:
take the counter and the defaultString outside the function. If the defaultString gets a value dynamically rather than what you showed in your code, extract its value on page load or any other event like change, etc. rather than passing it inside the function.
Do not assign a new string the result of your manipulation. Instead, assign it to defaultString. This way you probably won't need an if-else loop and a newLastChar to take care of newer results.
Manipulate the assignment to the element accordingly.
You can use Javascript closure functionality.
var scrollString = (function() {
var defaultString = "Learning to Code Javascript Rocks!";
return function() {
// convert the string into array, so that you can use the splice method
defaultString = defaultString.split('');
// get last element
var lastElm = defaultString.splice(defaultString.length - 1, defaultString.length)[0];
// insert last element at start
defaultString.splice(0, 0, lastElm);
// again join the string to make it string
defaultString = defaultString.join('');
document.getElementById('Result').innerHTML = defaultString;
return defaultString;
}
})();
Using this you don't need to declare any variable globally, or any counter element.
To understand Javascript Closures, please refer this:
http://www.w3schools.com/js/js_function_closures.asp
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
I am learning about the new features in ES6. I have a question about let and it concerns this code:
for (let i = 0; i < 45; i++) {
var div = document.createElement('div');
div.onclick = function() {
alert("you clicked on a box #" + i);
};
document.getElementsByTagName('section')[0].appendChild(div);
}
I am confused by this code. What is happening with that div object that is declared at the start of each loop? Is that a brand new, separate object each time, somehow enclosed in the block scope of i? Or is this div object being overwritten each pass through the loop and if so, how does it maintain it's connection to the i it is given via let?
When I like to get a better understanding of what's happening in ES6 code, I enter my Javascript into the BabelJS REPL.
Your code when entered into the REPL outputs:
'use strict';
var _loop = function (i) {
div = document.createElement('div');
div.onclick = function () {
alert("you clicked on a box #" + i);
};
document.getElementsByTagName('section')[0].appendChild(div);
};
for (var i = 0; i < 45; i++) {
var div;
_loop(i);
};
Because you used let to assign i, its value is only available in the scope of the loop (or the function in the Babel example) for each loop iteration. To get the same functionality for the div variable, you could assign that variable in the loop body.
for (let div, i = 0; i < 45; i++) {
div = document.createElement('div');
...
}
Lastly, about closures and holding on to the i variable, you're one step away from creating a closure to maintain the current i value for each div.
// Create a function to hold on to a specific number
function createOnClick(index) {
return function() {
alert("you clicked on a box #", index);
};
};
// Assign the function to the element's action
div.onClick = createOnClick(i);
Without the function factory, the onClick value would always get the maximum i value of 44. This is because the function is being run after the entire loop has iterated and i has stopped at i < 45.
The <div> is a brand new object in each iteration, but it isn't enclosed in the block scope of i.
The function expression that is attached to the div is however also a brand new object, but this object is closing over i.
On each iteration div is a branch new object element.
Let defines i to be accessible only within foreach loop (What's the difference between using "let" and "var" to declare a variable?).
Once the div element is appended to "section" element and for loop starts a new iteration, you lose a reference to recently appended div by overriding it with a new one.
Getting the negative value when i perform the onclick function in javascript
function sun()
{
var d,i;
var t = document.getElementById("table");
var rows = t.getElementsByTagName("tr");
for (i = 0; i < rows.length; i++) {
console.log("inside............." + i);
rows[i].onclick = function() {
d = (this.rowIndex);
console.log(d);
};
}
}
Though I'm not sure it's what's causing the exact issue you're noticing, you've encountered a pretty common JavaScript pitfall here by using a closure (anonymous function) inside of a loop. JavaScript, like many other languages that support functional programming, has the convenient property that functions can "close scope" around any variables visible to them at the time of their creation. So, as you've done there, you can use the value of d (or i) inside your function so long as it can see them when your function is declared.
Something funny happens inside a loop, though: every function you create within the loop shares the same scope, meaning they all share the exact same copies of d and i. As a result, when you click on any of your rows, the values of d and i used will be their values at the end of the loop, not the particular iteration you're targeting.
This is ordinarily fixed using something known as the "generator pattern," where you create a separate function that returns new functions closed over your desired scope. For example, in your code, you might do something like
function generateClickHandler(i, d) {
return function() {
d = (this.rowIndex);
console.log(d);
};
}
function sun()
{
var d,i;
var t = document.getElementById("table");
var rows = t.getElementsByTagName("tr");
for (i = 0; i < rows.length; i++) {
console.log("inside............." + i);
rows[i].onclick = generateClickHandler(i, d);
}
}
The new function generateClickHandler returns a function itself, but the important thing to notice here is that the returned function closes over the local arguments i and d, not the shared i and d values used in the loop — their values get copied when you call generateClickHandler. In this way, your code won't be subject to strange closure effects.