Drawing a set of circle with RaphaelJS library.
For each circle I want to create an onclick function that passes a variable, but in this function my variable is undefined.
What is the problem?
This is my code:
//children is an array such as [1,2,4[5,6,7]]
for (var i = 0; i < children.length; i++) {
var array = children;
alert("ARRAY[0]===" + array[0])
var st = space2Draw.set();
st.push(space2Draw.circle(xChildren, yChildren, 20).click(function (array) {
//array[i] is undefined
alert("ARRAY[i]===" + array[i]);
//retrive(array[i]);
}),
LineRoot(xRadice, yRadice, xChildren, yChildren, space2Draw));
space2Draw.text(xChildren, yChildren, children[i]).attr({
fill: "white"
});
st.attr({
fill: "red"
});
xChildren += 50;
}
You shouldn't have a "array" parameter to your click callback, it is overriding the "array" var from the parent scope. You can just remove the argument and it should be ok.
I think you will have another problem with i always being children.length in your click callback (because the function scope will actually be closed at the end of the loop. More info here). You should create an auxiliary function to create the callback for you.
You could try something like this:
//children is an array such as [1,2,4[5,6,7]]
for (var i = 0; i < children.length; i++) {
var array = children;
alert("ARRAY[0]===" + array[0])
var st = space2Draw.set();
st.push(space2Draw.circle(xChildren, yChildren, 20).click(getCallback(array , i)),
LineRoot(xRadice, yRadice, xChildren, yChildren, space2Draw));
space2Draw.text(xChildren, yChildren, children[i]).attr({
fill: "white"
});
st.attr({
fill: "red"
});
xChildren += 50;
}
function getCallback(array , i){
return function () {
alert("ARRAY[i]===" + array[i]);
}
}
You should do this:
...
st.push(space2Draw.circle(xChildren, yChildren, 20).click((function (array, i) {
return function () {
//array[i] is undefined
alert("ARRAY[i]===" + array[i]);
//retrive(array[i]);
}
}(array, i)));
...
The reason why this works is that in javascript, scope is defined by functions, not by blocks.
Your callback gets access (by closure) to the array and i (among other variables), but when the callback gets executed, i is equal to children.length, because the loop has finished iterating.
By using IIFE (immediately invocation function expression) we create a new scope, and the callback gets access to the current value of array and i.
Also, the click function calls the callback by passing an event object as first parameter, so our IIFE should return this
return function (event) {
// access array and i by closure
// event is the event representing the click
// event, provided by Raphael
// do something
}
Related
I'm trying to use the following code in Javascript. I'd like to pass a function rulefunc() a number of times into the onChange() function iteratively. I want to be able to access i from within the function when it is called. How can I do this?
var gui = new DAT.GUI();
for for (var i=0; i<5; i++) {
// want to associate ruleFunc with i
gui.add(lsys, 'grammarString').onChange(ruleFunc);
}
function ruleFunc(newVal) {
...
// access i here
}
At the event side:
Here since for loop is synchronous an IIFE is used so that right value of i is passed
IIFE and the onchange event makes a closure which makes the right value of i to be passed
at the argument
At the event callback side
Closure is used so that the function that is returned can access the value of the argument
var gui = new DAT.GUI();
for (var i=0; i<5; i++) {
// want to associate ruleFunc with i
(function(a){ //making an IIFE to make sure right value of i is passed to the function
f1.add(lsys, 'grammarString').onChange(ruleFunc(a));
})(i);
}
function ruleFunc(newVal) {
return function(){
//making a closure which will have access to the argument passed to the outer function
console.log(newVal);
}
}
You can write a function that returns a function like
function ruleFunc(i) {
return function(newVal){
// ... here use your newVal and i
}
}
And use like
f1.add(lsys, 'grammarString').onChange(ruleFunc(i));
You call the function that gets the i from the outer scope and then the returned function gets the newVal of the 'onChange' event. Note that I am calling the function ruleFunc and pass a parameter i. Inside the function you can now use your i variable and newVal.
Example of how it works. Here I add functions to the array with approprite i values. After that when I execute each function, it properly knows what i was used when it have been created. It is called closure.
var functions = [];
function ruleFunc(newVal) {
return function (){
console.log(newVal);
};
}
for(var i = 0; i < 5; i++) {
functions.push(ruleFunc(i));
}
for(var i = 0; i < functions.length; i++) {
functions[i]();
}
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
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);
}
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);
}
I know that this code doesn't work and I also know why.
However, I do not know how to fix it:
JavaScript:
var $ = function(id) { return document.getElementById(id); };
document.addEventListener('DOMContentLoaded', function()
{
for(var i = 1; i <= 3; i++)
{
$('a' + i).addEventListener('click', function()
{
console.log(i);
});
}
});
HTML:
1
2
3
I want it to print the number of the link you clicked, not just "4".
I will prefer to avoid using the attributes of the node (id or content), but rather fix the loop.
Wrap the loop block in its own anonymous function:
document.addEventListener('DOMContentLoaded', function()
{
for(var i = 1; i <= 3; i++)
{
(function(i) {
$('a' + i).addEventListener('click', function() {
console.log(i);
})
})(i);
}
}
This creates a new instance of i that's local to the inner function on each invocation/iteration. Without this local copy, each function passed to addEventListener (on each iteration) closes over a reference to the same variable, whose value is equal to 4 by the time any of those callbacks execute.
The problem is that the inner function is creating a closure over i. This means, essentially, that the function isn't just remembering the value of i when you set the handler, but rather the variable i itself; it's keeping a live reference to i.
You have to break the closure by passing i to a function, since that will cause a copy of i to be made.
A common way to do this is with an anonymous function that gets immediately executed.
for(var i = 1; i <= 3; i++)
{
$('a' + i).addEventListener('click', (function(localI)
{
return function() { console.log(localI); };
})(i);
}
Since you're already using jQuery, I'll mention that jQuery provides a data function that can be used to simplify code like this:
for(var i = 1; i <= 3; i++)
{
$('a' + i).data("i", i).click(function()
{
console.log($(this).data("i"));
});
}
Here, instead of breaking the closure by passing i to an anonymous function, you're breaking it by passing i into jQuery's data function.
The closure captures a reference to the variable, not a copy, which is why they all result in the last value of the 'i'.
If you want to capture a copy then you will need to wrap it in yet another function.