I have a timeout which calls a function until 100% progress is complete. It then executes the function I have assigned to it. Only the value that was given to it is undefined or at least part of it.
I'm not sure at which stage the code is losing the value being passed, thus making it return undefined but I have made a JS Fiddle with it in action for you to see it:
JS Fiddle
My end result is to receive the value correct then remove the given element like so:
function rmv_div(div_id) {
//div_id is not properly defined so cannot find the div.
document.getElementById('result').innerHTML = div_id;
var div = document.getElementById(div_id);
div.parentNode.removeChild(div);
}
The problem is that the variable i used inside func is created outside the scope of that function, and is increased at each iteration. Then, when you call func at the end, i equals array.length, so array[i] is undefined.
You can solve it creating another variable at each iteration that you won't increase:
Solution 1:
Demo: http://jsfiddle.net/qJ42h/4/ http://jsfiddle.net/qJ42h/11/
for (var i = 0; i < array.length; i++) {
var bar = document.getElementById('bar' + array[i]),
text = document.getElementById('text' + array[i]),
remove = 'wrap' + array[i],
j = i;
do_something(bar, text, function () {
rmv_div('id' + array[j]);
}, 1);
}
Solution 2
Demo: http://jsfiddle.net/qJ42h/8/ http://jsfiddle.net/qJ42h/12/
for (var i = 0; i < array.length; i++) {
var bar = document.getElementById('bar' + array[i]),
text = document.getElementById('text' + array[i]),
remove = 'wrap' + array[i];
do_something(bar, text, (function(i) {
return function(){ rmv_div('id' + array[i]); }
})(i), 1);
}
The problem here is that you didn't isolate the loop variable i inside the closure. However, this can be solved much more elegantly by using objects.
First off, I'm introducing the object that will encapsulate what you want; it gets initialized with a bar element and a function to call when it's done counting to 100. So, I'll call it BarCounter:
function BarCounter(element, fn)
{
this.element = element;
this.fn = fn;
this.text = element.getElementsByTagName('div')[0];
this.counter = 0;
}
This is just the constructor; it doesn't do anything useful; it resolves the text element, which is simply the first <div> tag it can find underneath the given element and stores that reference for later use.
Now we need a function that will do the work; let's call it run():
BarCounter.prototype.run = function()
{
var that = this;
if (this.counter < 100) {
this.text.innerHTML = this.counter++;
setTimeout(function() {
that.run();
}, 70);
} else {
this.fn(this.element);
}
}
The function will check whether the counter has reached 100 yet; until then it will update the text element with the current value, increase the counter and then call itself again after 70 msec. You can see how the reference to this is kept beforehand to retain the context in which the run() function is called later.
When all is done, it calls the completion function, passing in the element on which the BarCounter object operates.
The completion function is much easier if you pass the element to remove:
function removeDiv(element)
{
element.parentNode.removeChild(element);
}
The final step is to adjust the rest of your code:
var array = [1];
for (var i = 0; i < array.length; ++i) {
var bar = new BarCounter(
document.getElementById('bar' + array[i]),
removeDiv
);
bar.run();
}
It's very simple now; it creates a new BarCounter object and invokes its run() method. Done :)
Btw, you have the option to remove the element from within the object as well; this, of course, depends on your own needs.
Demo
Related
I have 10 rows in a table that contains the address of each tag on a map. I am trying to add a click event to each tag on the map to it's corresponding table row. Instead of writing each click event separately I'm attempting to do it through a for loop. What's the best way to do this? The for loop below works for only the last iteration but not for all.
for (var i=0; i < 10; i++) {
var maptag = "#maptag";
var maprow = "#maprow";
$(maptag + +i).click(function() {
console.log('in the hole!');
$(maprow + +i).toggleClass('highlight');
return false;
});
You may need a closure
for (var i = 0; i < 10; i++) {
var maptag = "#maptag";
var maprow = "#maprow";
(function(x) { // IIFE
$(maptag + +x).click(function() {
console.log('in the hole!');
$(maprow + +x).toggleClass('highlight');
return false;
})
}(i))
}
Your problem is scoping. What's happening is when you create your click event, i is the current value in the loop. However, when the click even is triggered, that i is set to the state it was at the end of the loop (i.e., your last one).
What you need to do is wrap it in a function to affect the scope. Something like this:
for (var i=0; i < 10; i++) {
var maptag = "#maptag";
var maprow = "#maprow";
$(maptag + i).click((function (i) { return function() {
console.log('in the hole!');
$(maprow + i).toggleClass('highlight');
})(i));
}
This looks a little weird, but what you're doing is wrapping the function that'll actually trigger in another function, which is self-calling:
(function (i) {
// will trigger immediately
}(i);
You pass in your i there, but then the function inside which is returned uses an i which is actually a different variable. The inner i doesn't change when the loop changes, so it'll have the value you expect when the click is actually triggered.
P.S., never seen variableName + +i before. Not sure if that was a typo, or some special syntax I'm unaware of, but if it is, it'd seem unnecessary here. Your return false is also unnecessary, as without it, it'll implicitly return null which is also a falsey value (unless you're explicitly checking for false somewhere with an ===).
What is the difference between these functions? Why does the first one work and the second does not work as expected?
http://jsfiddle.net/GKDev/x6pyg/ (this works)
http://jsfiddle.net/GKDev/bv4em/ (and this is not)
I'm trying to loop over input elements and add onfocus events on them:
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
};
}
In your non-working example, when the anonymous function is called, item has the last value it held when the for loop finished executing. That's because this variable belongs to the parent function that contains the for loop.
In your working example, you create a new function, pass in the current value of item like so:
function (help) {
return function () {
showHelp(help); // <-- This will be the value enclosed in this anonymous function
};
}(item.help); // <-- Calls an anonymous function passing in the current value
This creates a new closure around that value as it existed during that iteration. When the anonymous function is called, it uses that local value.
It is treated as:
var item;
for (var i = 0; i < helpText.length; i++) {
item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
};
}
The loop has finished before any focus callback fires and at that point item is the last item assigned.
It is pretty easy in fact.
In the first case you are passing item.help inside of the closure, which acts as a local copy. It's like a prisoner of its scope. What happens outside, nobody cares.
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function (help) {
return function () {
showHelp(help);
};
}(item.help);
}
In the second one, there is no closure preserving the value of item, which means that when item is evaluated, it evaluates to its actual value, i.e.: the last element of the array, since the last loop of the for set its value to the last value of the array.
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
I have the following code that adds an onmouseover event to a bullet onload
for (var i = 0; i <= 3; i++) {
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
}
This is the function that it is calling. It is supposed to add a css class to the menu item as the mouse goes over it.
function addBarOnHover(node) {
document.getElementById('menu').getElementsByTagName('li')[node].className = "current_page_item"; }
When the function is called, I keep getting the error:
"document.getElementById("menu").getElementsByTagName("li")[node] is
undefined"
The thing that is stumping me is I added an alert(node) statement to the addBarOnHover function to see what the value of the parameter was. The alert said the value of the parameter being passed was 4. I'm not sure how this could happen with the loop I have set up.
Any help would be much appreciated.
This is a common problem when you close over an iteration variable. Wrap the for body in an extra method to capture the value of the iteration variable:
for (var i = 0; i <= 3; i++) {
(function(i){ //here
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
})(i); //here
}
an anonymous function is created each time the loop is entered, and it is passed the current value of the iteration variable. i inside the anonymous function refers to the argument of this function, rather than the i in the outer scope.
You could also rename the inner variable for clarity:
for(var i=0; i<=3; i++){
(function(ii){
//use ii as i
})(i)
}
Without capturing the iteration variable, the value of i when it is finally used in the anonymous handler has been already changed to 4. There's only one i in the outer scope, shared between all instances of the handler. If you capture the value by an anonymous function, then the argument to that function is used instead.
i is being passed as a reference (not by value), so once the onmouseover callback is called, the value of i has already become 4.
You'll have to create your callback function using another function:
var menu = document.getElementById('menu');
var items = menu.getElementsByTagName('li');
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = (function(i) {
return function() {
addBarOnHover(i);
};
})(i);
}
You could make it a little more readable by making a helper function:
var createCallback = function(i) {
return function() {
addBarOnHover(i);
};
};
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = createCallback(i);
}
When I run this sample code in Google Chrome, the intended behavior--loading an image within a placeholder image tag on the current page--does not occur. I checked the value of currPic when showPic() is called, and it is "undefined." I know if I change the parameter to showPic from 'anchors[i]' to 'this', then it will work, but was trying to understand why this is so.
function showPic(currPic) {
var srcLoc = currPic.getAttribute("href");
var placeHolder = document.getElementById("placeholder");
placeHolder.setAttribute("src", srcLoc);
var imgLabel = document.getElementById("imglabel");
var currLinkTitle = currPic.getAttribute("title");
imgLabel.firstChild.nodeValue = currLinkTitle;
}
function prepareGallery() {
if(!(document.getElementsByTagName && document.getElementById)) return false;
var imgGallery = document.getElementById("imagegallery");
if(imgGallery) {
var anchors = imgGallery.getElementsByTagName("a");
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = function() {
showPic(anchors[i]);
return false;
}
}
}
}
Inside the anonymous function, anchors[i] provides a runtime reference. At the time the click occurs, anchors[i] no longer exists. While it existed at the time the assignment was made, it falls out of scope at the time of the click (since it's just an array reference). However, using this provides a solid reference to the immediate object that is always available at the time of the click.
More succinctly, anchors[i] is a reference to a position in an array (which leaves scope once the for loop exits). this is a reference to the dom element itself.
Because this would also work: showPic(document.getElementById(anchors[i].id)); - do you "get" it now (pun very much intended)?
Didn't see the obvious statement regarding how closures work, so here's my take on it.
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = function() {
showPic(anchors[i]);
return false;
}
}
Notice how you reference the i variable inside the loop? By the end of your loop, the value of i equals anchors.length.
So, when any of your onclick function is executed, that reference to i now points one position past the last index of anchors; this is why you see currPic is undefined.
One solution to this problem has been given in other answers: use this to reference the current anchor and don't pass anchors[i] to the onclick function.
As you may encounter similar situations, I'll show you another solution by closing over the value of i like so:
var i;
for(i = 0; i < anchors.length; i++) {
anchors[i].onclick = (function(i) {
// inside this function, i is closed over and won't change anymore
return function() {
showPic(anchors[i]);
return false;
}
}(i));
}
I have this function
function createSlidingGallery(){
gallery_position = parseInt(jQuery(".imageWrapper.default").attr("rel"));
gallery_position_min = 0;
gallery_position_max = parseInt(jQuery("#galleryEl .sig_thumb a").size() - 1);
var galleryWrapper = document.createElement("div");
galleryWrapper.className = "sGalleryWrapper";
for (var i = 0; i < gallery_position_max; i++) {
var slide = document.createElement("div");
slide.className = "slide slide"+(i);
slide.setAttribute('rel', i);
galleryWrapper.appendChild(slide);
};
jQuery(".imageWrapper.top").append(galleryWrapper);
//HERE COMES THE PROBLEM PART
for (var i = 0; i < gallery_position_max; i++) {
var position = i;
//THE CALLBACK ACTUALLY USES THE SAME CONTEXT FOR ALL PARTICULAR CALLS SO THAT THE POSITION VALUE HOLDS THE MAXIMUM VALUE IN ALL INSTANCES
loadImage(position, false, function(index){
console.log(index);
jQuery(".slide"+position).css({
'background': 'url('+backgroundImages[index].src+')'
});
});
};
hideLoadingDC();
}
What it should do is asynchronously load images into dynamicaly created elements. It actually creates all the elements, and it loads images as well. But there is function called loadImage which is intended to preload images and then save the information that this images has been already loaded and are propably cached. I am calling it with callback function which handles the loaded image and sets it as a background to appropriate element.Hence I need to hold the information about the element (like pointer, or index/position).
Now I am trying to propagate its index to the function, but because the callback function is called after some time the position variable has already other value (the for loop actually goes through and in all calls of the callback it is set to the maximum value)
I know I can alter the loadImage function and add the position as another attribute, but I would prefere any other solution. I do not want to alter the loadImage function.
You can use a helper function to create a new scope for the position variable:
function makeGalleryCallback(position) {
return function(index){
console.log(index);
jQuery(".slide"+position).css({
'background': 'url('+backgroundImages[index].src+')'
});
};
}
function createSlidingGallery(){
...
for (var i = 0; i < gallery_position_max; i++) {
loadImage(i, false, makeGalleryCallback(i));
}
...
}
The problem is that all the callback functions are referencing the same i value, and not actually tracking the value at the time of iteration. So, you need to create a new scope (a closure) that creates a new reference for each different value of i.
A new scope can be created in JS with a function. Your code needs to be wrapped with an anonymous function and execute that function for each different value of i.
for (var i = 0; i < gallery_position_max; i++) {
var position = i;
loadImage(position, false, (function(index){
return function(){
console.log(index);
jQuery(".slide"+position).css({
'background': 'url('+backgroundImages[index].src+')'
});
};})(i));
};