parameter error with addEventListener - javascript

Okey so I got this following javaScript code.
function test(id)
{
alert(id);
}
var elem = document.getElementsByClassName('outsideDiv');
for(var i=0; i < elem.length; i++)
{
elem[i].addEventListener('mouseover', function(){test(i);}, false);
}
this gives all divs with the class a mouse over but the function always returns the latest i index. in this case i got 5 div elements and the alert is allways 5 no mather witch one i hover. Can anyone explain why?

Try using this instead:
function mouseOverFunc(i) {
return function () {
test(i);
};
}
function test(id) {
alert(id);
}
var elem = document.getElementsByClassName('outsideDiv');
for(var i=0; i < elem.length; i++) {
elem[i].addEventListener('mouseover', mouseOverFunc(i), false);
}
Just because you add event listeners to the elements doesn't mean the value of i is preserved for each listener. You need to create a closure that will create a new scope with i.
The reason this is happening is because the function bound to each listener is just a reference. When the event happens (mouseover), the function is finally called, but what's the value of i? The for loop finished executing a long time ago, so the value of i is the end value - 5.

Related

Javascript check in array if string is found not working with indexOf [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
I've got the following code snippet.
function addLinks () {
for (var i=0, link; i<5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
link.onclick = function () {
alert(i);
};
document.body.appendChild(link);
}
}
The above code is for generating 5 links and binding each link with an alert event to show the current link id. But It doesn't work. When you click the generated links they all say "link 5".
But the following code snippet works as our expectation.
function addLinks () {
for (var i=0, link; i<5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
link.onclick = function (num) {
return function () {
alert(num);
};
}(i);
document.body.appendChild(link);
}
}
The above 2 snippets are quoted from here. As the author's explanation seems the closure makes the magic.
But how it works and How closure makes it work are all beyond my understanding. Why the first one doesn't work while the second one works? Can anyone give a detailed explanation about the magic?
Quoting myself for an explanation of the first example:
JavaScript's scopes are function-level, not block-level, and creating a closure just means that the enclosing scope gets added to the lexical environment of the enclosed function.
After the loop terminates, the function-level variable i has the value 5, and that's what the inner function 'sees'.
In the second example, for each iteration step the outer function literal will evaluate to a new function object with its own scope and local variable num, whose value is set to the current value of i. As num is never modified, it will stay constant over the lifetime of the closure: The next iteration step doesn't overwrite the old value as the function objects are independant.
Keep in mind that this approach is rather inefficient as two new function objects have to be created for each link. This is unnecessary, as they can easily be shared if you use the DOM node for information storage:
function linkListener() {
alert(this.i);
}
function addLinks () {
for(var i = 0; i < 5; ++i) {
var link = document.createElement('a');
link.appendChild(document.createTextNode('Link ' + i));
link.i = i;
link.onclick = linkListener;
document.body.appendChild(link);
}
}
We have 5 divs on the page, each with an ID ... div1, div2, div3, div4, div5
jQuery can do this ...
for (var i=1; i<=5; i++) {
$("#div" + i).click ( function() { alert ($(this).index()) } )
}
But really addressing the problem (and building this up slowly) ...
STEP 1
for (var i=1; i<=5; i++) {
$("#div" + i).click (
// TODO: Write function to handle click event
)
}
STEP 2
for (var i=1; i<=5; i++) {
$("#div" + i).click (
function(num) {
// A functions variable values are set WHEN THE FUNCTION IS CALLED!
// PLEASE UNDERSTAND THIS AND YOU ARE HOME AND DRY (took me 2 years)!
// Now the click event is expecting a function as a handler so return it
return function() { alert (num) }
}(i) // We call the function here, passing in i
)
}
SIMPLE TO UNDERSTAND ALTERNATIVE
If you can't get your head around that then this should be easier to understand and has the same effect ...
for (var i=1; i<=5; i++) {
function clickHandler(num) {
$("#div" + i).click (
function() { alert (num) }
)
}
clickHandler(i);
}
This should be simple to understand if you remember that a functions variable values are set when the function is called (but this uses the exact same thought process as before)
Basically, in the first example you're binding the i inside the onclick handler directly to the i outside the onclick handler. So when the i outside the onclick handler changes, the i inside the onclick handler changes too.
In the second example, instead of binding it to the num in the onclick handler, you're passing it into a function, which then binds it to the num in the onclick handler. When you pass it into the function, the value of i is copied, not bound to num. So when i changes, num stays the same. The copy occurs because functions in JavaScript are "closures", meaning that once something is passed into the function, it's "closed" for outside modification.
Others have explained what's going on, here's an alternative solution.
function addLinks () {
for (var i = 0, link; i < 5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
with ({ n: i }) {
link.onclick = function() {
alert(n);
};
}
document.body.appendChild(link);
}
}
Basically the poor mans let-binding.
In the first example, you simply bind this function to the onclick event:
function() {alert(i);};
This means that on the click event js should alert the value of the addlink functions i variable. Its value will be 5 because of the for loop().
In the second example you generate a function to be bound with another function:
function (num) {
return function () { alert(num); };
}
This means: if called with a value, return me a function that will alert the input value. E.g. calling function(3) will return function() { alert(3) };.
You call this function with the value i at every iteration, thus you create separate onclick functions for each links.
The point is that in the first example your function contained a variable reference, while in the second one with the help of the outer function you substituted the reference with an actual value. This is called a closure roughly because you "enclose" the current value of a variable within your function instead of keeping a reference to it.

Getting the right index of an object in a loop in javascript

I have a list of nodes and I am going to draw each node using a raphael object. I have the following loop:
for(var i=0; i<curNodes.length; i++){
var node = curNodes[i];
var obj = paper.rect(node.getX(), node.getY(), node.width, node.getHeight())
.attr({fill:nodes[i].getColor(), "fill-opacity": 0.6})
.click(function(e){ onMouseClicked(i,e); });
}
on click, I want to call a function which can view some data associated with 'i' th element of curNodes array. However, all the time the last 'i' is passed to this function. my function is:
var onMouseClicked = function(i, event){
switch (event.which) {
case 1:
this.attr({title: curNodes[i].name});
break;
}
}
How should I access the correct index when calling a function?
Try this:
.click((function (i) {
return function (e) {
onMouseClicked(i,e);
};
})(i));
Like you noticed, the value of i (or the parameter in your function) is the last index from the for loop. This is because the click event doesn't happen immediately - the binding does, but the value of i is not captured. By the time the click handler actually is executed (when the click event is triggered in whatever way), the for loop has completed (a long time ago), and the value of i is the final value of the loop. You can capture the value of i with a closure like the above.
Although I know another way of handling, cleaner in my opinion, is to use:
.click(clickHandler(i));
function clickHandler(index) {
return function (e) {
onMouseClicked(i, e);
};
}
It is because of the closure variable i.
for (var i = 0; i < curNodes.length; i++) {
(function(i) {
var node = curNodes[i];
var obj = paper.rect(node.getX(), node.getY(), node.width,
node.getHeight()).attr({
fill : nodes[i].getColor(),
"fill-opacity" : 0.6
}).click(function(e) {
onMouseClicked(i, e);
});
})(i);
}

Wrong Parameter is passed to function

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);
}

Why does my anonymous function not have the context of the current object in this example?

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));
}

Javascript Closure Problem

I know this kind of question gets asked alot, but I still haven't been able to find a way to make this work correctly.
The code:
function doStuff () {
for (var i = 0; i< elementsList.length; i++) {
elementsList[i].previousSibling.lastChild.addEventListener("click", function(){
toggle(elementsList[i])}, false);
}
} // ends function
function toggle (element) {
alert (element);
}
The problem is in passing variables to the toggle function. It works with the this keyword (but that sends a reference to the clicked item, which in this case is useless), but not with elementsList[i] which alerts as undefined in Firefox.
As I understood it, using anonymous functions to call a function is enough to deal with closure problems, so what have I missed?
Try:
function startOfFunction() {
for (var i = 0; i< elementsList.length; i++) {
elementsList[i].previousSibling.lastChild.addEventListener(
"click",
(function(el){return function(){toggle(el);};})(elementsList[i]),
false
);
}
} // ends function
function toggle (element) {
alert (element);
}
The Problem is, that you want to use the var i! i is available in the onClick Event, (since closure and stuff). Since you have a loop, i is counted up. Now, if you click on any of the elements, i will always be elementsList.length (since all event functions access the same i )!
using the solution of Matt will work.
As an explanation: the anonymous function you use in the for loop references the variable "i" to get the element to toggle. As anonymous functions use the "live" value of the variable, when somebody clicks the element, "i" will always be elementsList.length+1.
The code example from Matt solves this by sticking the i into another function in which it is "fixated". This always holds true:
If you iterate over elements attaching events, do not use simple anonymous functions as they screw up, but rather create a new function for each element. The more readable version of Matts answer would be:
function iterate () {
for (var i = 0; i < list.length; i++) {
// In here, i changes, so list[i] changes all the time, too. Pass it on!
list[i].addEventListener(createEventFunction(list[i]);
}
}
function createEventFunction (item) {
// In here, item is fixed as it is passed as a function parameter.
return function (event) {
alert(item);
};
}
Try:
function doStuff () {
for (var i = 0; i< elementsList.length; i++) {
(function(x) {
elementsList[x].previousSibling.lastChild.addEventListener("click", function(){
toggle(elementsList[x])}, false);
})(i);
}
} // ends function
I think it might be an issue with passing elementsList[i] around, so the above code has a closure which should help.

Categories