JavaScript variables do not persist in jQuery event handler [duplicate] - javascript

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
For some reason, I'm not understanding the scope of local and global variables with relation to jQuery click handlers. When I click the buttons, I'm getting "undefined", but I expect it to show the category of the button clicked. How do I address this?
I've setup an example to show my confusion. Fiddle here: http://jsfiddle.net/zvhsqeyn/
Simple html buttons:
<button class="hobbies">hobbies</button>
<button class="sledding">sledding</button>
<button class="personal">personal</button>
<button class="food">food</button>
Some javascript to handle the event where the buttons are clicked.
var GLOBAL_CATEGORIES = ['hobbies', 'sledding'];
$(window).load(function() {
var categories = ['personal', 'food'];
// local categories
for (var i = 0; i < categories.length; i++) {
$('.' + categories[i]).click(function() {
alert(categories[i]);
return false;
});
}
// Global categories
for (var i = 0; i < GLOBAL_CATEGORIES.length; i++) {
$('.' + GLOBAL_CATEGORIES[i]).click(function() {
alert(GLOBAL_CATEGORIES[i]);
return false;
});
}
});
Edit:
After taking in the feedback, here is the properly working http://jsfiddle.net/zvhsqeyn/1/

This has to do with the nature of closures.
// local categories
for (var i = 0; i < categories.length; i++) {
$('.' + categories[i]).click(function() {
alert(categories[i]);
return false;
});
}
You're assigning an anonymous function as the click handler for each of these buttons. That function has access to the variable i, because it's visible in the parent scope. However - by the time you click one of these buttons, your loop has long since finished, and the value of i is now equal to categories.length. There isn't any element there, so your click functions return undefined.
In general, it's not safe to rely on the value of index variables when assigning event handlers in a loop. As a workaround, you can use the jquery $.each function:
$.each(categories, function(index, value) {
// local categories
$('.' + value).click(function() {
alert(value);
return false;
});
});
This works because it creates a separate closure for each click handler function.

This is a very common ish problem and here is the explanation
If we follow the process through what you are doing is adding a click listener to the ith GLOBAL_CATEGORIES and then moving on to the next one. You do this until i is too big for the GLOBAL_CATEGORIES array.
You have to remember that i has only been set once and so now i is 1 more than the length of GLOBAL_CATEGORIES. This means that when the alert fires GLOBAL_CATEGORIES[i] is undefined because i is too big.
A better way to do this is with the jQuery each syntax like below
$.each(GLOBAL_CATEGORIES, function(i, category) {
$('.' + category).click(function() {
alert(category);
return false;
});
});

Related

JQuery: on click listener doesn't keep instanced variables from loop [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 5 years ago.
I'm making a point and click adventure/dating sim like website. I am currently making Dialogue Options. Options that come up at a certain point in conversation that allows the user to select their response, or chose some sort of prompt option.
My entire system seems to work fine except one step where I'm populating the Dialogue Options onto the prompt.
//Run through up to 10 dialogue options and add to dialogue option window.
for(var i = 1; i<3; i++)
{
var Option = eval(O['o'+i]);
if(typeof(Option) !== 'undefined')
{
$("#CAM_OptionsFrame").append("<p id='DialogueO"+i+"' class='clickable'>"+Option[0]+"</p>");
var funcString = Option[1];
var func = window[funcString];
$('#DialogueO'+i).on("click",function() {
$( this ).off("click");
endDialogueOption(time, O.noHide, function(){func();});
});
}
}
I load any number of Dialogue Options and their partner functions from an argument. Then loop through them listing the name in the HTML of the prompt and giving them a unique ID.
Then I select the element by the unique ID and give it an onclick listener which refers to the function passed in by the specific option.
However, ALL the elements run the last options function when you click them. I suspect the listener points to the "func" variable and updates every time I change it in the loop.
How do I make the listener refer to the instance of the variable so that every new listener I make refers to it's own "func" variable?
This is a classic closure issue. You have 2 options, once is invoke each function with the current index of the loop.
for (var i = 1; i < 3; i++) {
(function(index) {
var Option = eval(O['o' + index]);
if (typeof(Option) !== 'undefined') {
$("#CAM_OptionsFrame").append("<p id='DialogueO" + index + "' class='clickable'>" + Option[0] + "</p>");
var funcString = Option[1];
var func = window[funcString];
$('#DialogueO' + index).on("click", function() {
$(this).off("click");
endDialogueOption(time, O.noHide, function() {
func();
});
});
}
})(i)
}
If you have ES6 support in the code base, then the only thing you need to change, if var to let in the for loop.
for (let i = 1; i < 3; i++) {
.. everything else remains the same.

using the loop index value in JS [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
I have the javascript function in my jsp page as
<script type="text/javascript">
$(document).ready(function(){
for(i=0; i<20; i++ ) {
$(".plus"+i).click(function(){
$(".details"+i).slideToggle(500)
});
}
});
for each iteration i want the output like this:
.plus0
.details0
Next iteration :
.plus1
.details1 and so on. But this is not working. Please help.
There will be no error in the console. The issue is that by the time the click handler has been triggered, the for() loop has already completed, so i will always equal 19.
You can circumvent this behaviour using event data in jQuery. You can update your code as follows:
$(function() {
for( i = 0; i < 20; i++ )
{
$('.plus' + i).click( { theIndex : i }, function(e) {
$('.details' + e.data.theIndex).slideToggle(500)
});
}
});
The problem is caused because you are using closures. The reference of i is held by the click event handler. So the latest value of i is seen by all event handlers.
To solve the problem, write a factory function that returns an event handler.
var createEventHandler = function (param1) {
return function (e) {
$(".plus"+param1).click(function(){
$(".details"+param1).slideToggle(500)
});
}
}
$(document).ready(function(){
for(i=0; i<20; i++ ) {
$(".plus"+i).click(createEventHandler(i));
}
});
Read about closure variables.

generating dynamic onclick events with javascript

I am dynamically generating a series of onclick events where an alert() is associated with loop number of the pretended content. My problem is that currently the alerts outputs the 'i' value of the last loop rather than the i'th loop associated with the pretended content. Any thoughts?
JavaScript:
for (i = 1; i < 4; i++) {
prepend_content = 'foo';
$('#dynamic_div').prepend(prepend_content);
}
Many thanks.
Try concatenating it like you do before:
for (i = 1; i < 4; i++) {
prepend_content = 'foo';
$('#dynamic_div').prepend(prepend_content);
}
You might want to declare i and prepend_content (with var) in case you already haven't, to make sure they don't leak into the global scope.
At the same time, I wouldn't suggest using or adding HTML with inline event handlers. Try creating the element like this:
prepend_content = $("<a>").attr({
href: "#",
id: "img1_link_" + i
}).text("foo").on("click", (function (i) {
return function () {
alert(i);
};
})(i));
DEMO: http://jsfiddle.net/ujv4y/
The extra use of the immediately invoked function for the click handler is to make a closure that captures the value of i in the loop.
You can create a function using currying for the alert (for more complex stuff):
function(i) {
return function(){alert(i);}
}

Javascript multiple dynamic addEventListener created in for loop - passing parameters not working

I want to use event listeners to prevent event bubbling on a div inside a div with onclick functions. This works, passing parameters how I intended:
<div onclick="doMouseClick(0, 'Dog', 'Cat');" id="button_id_0"></div>
<div onclick="doMouseClick(1, 'Dog', 'Cat');" id="button_id_1"></div>
<div onclick="doMouseClick(2, 'Dog', 'Cat');" id="button_id_2"></div>
<script>
function doMouseClick(peram1, peram2, peram3){
alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}
</script>
However, I tried to create multiple event listeners in a loop with this:
<div id="button_id_0"></div>
<div id="button_id_1"></div>
<div id="button_id_2"></div>
<script>
function doMouseClick(peram1, peram2, peram3){
alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
for (var i=0; i<names.length; i++){
document.getElementById(names[i]).addEventListener("click", function(){
doMouseClick(i, "Dog", "Cat");
},false);
}
</script>
It correctly assigns the click function to each div, but the first parameter for each, peram1, is 3. I was expecting 3 different event handlers all passing different values of i for peram1.
Why is this happening? Are the event handlers not all separate?
Problem is closures, since JS doesn't have block scope (only function scope) i is not what you think because the event function creates another scope so by the time you use i it's already the latest value from the for loop. You need to keep the value of i.
Using an IIFE:
for (var i=0; i<names.length; i++) {
(function(i) {
// use i here
}(i));
}
Using forEach:
names.forEach(function( v,i ) {
// i can be used anywhere in this scope
});
2022 edit
As someone is still reading and upvoting this answer 9 years later, here is the modern way of doing it:
for (const [i, name] of names.entries()) {
document.getElementById(name).addEventListener("click", () => doMouseClick(i, "Dog", "Cat"), false);
}
Using const or let to define the variables gives them block-level scope and the value of i passed to the handler function is different for each iteration of the loop, as intended.
The old ways will still work but are no longer needed.
2013 answer
As pointed out already the problem is to do with closures and variable scope. One way to make sure the right value gets passed is to write another function that returns the desired function, holding the variables within the right scope. jsfiddle
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
function getClickFunction(a, b, c) {
return function () {
doMouseClick(a, b, c)
}
}
for (var i = 0; i < names.length; i++) {
document.getElementById(names[i]).addEventListener("click", getClickFunction(i, "Dog", "Cat"), false);
}
And to illustrate one way you could do this with an object instead:
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
function Button(id, number) {
var self = this;
this.number = number;
this.element = document.getElementById(id);
this.click = function() {
alert('My number is ' + self.number);
}
this.element.addEventListener('click', this.click, false);
}
for (var i = 0; i < names.length; i++) {
new Button(names[i], i);
}
or slightly differently:
function Button(id, number) {
var element = document.getElementById(id);
function click() {
alert('My number is ' + number);
}
element.addEventListener('click', click, false);
}
for (var i = 0; i < names.length; i++) {
new Button(names[i], i);
}
It's because of closures.
Check this out: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Creating_closures_in_loops_A_common_mistake
The sample code and your code is essentially the same, it's a common mistake for those don't know "closure".
To put it simple, when your create a handler function, it does not just accesses the variable i from the outer environment, but it also "remembers" i.
So when the handler is called, it will use the i but the variable i is now, after the for-loop, 2.
I've been struggling with this problem myself for a few hours and now I've just now managed to solve it. Here's my solution, using the function constructor:
function doMouseClickConstructor(peram1, peram2, peram3){
return new Function('alert("doMouseClick() called AND peram1 = ' + peram1 + ' AND peram2 = ' + peram2 + ' AND peram3 = ' + peram3 + ');');
}
for (var i=0; i<names.length; i++){
document.getElementById(names[i]).addEventListener("click", doMouseClickConstructor(i,"dog","cat"));
};
Note: I havn't actually tested this code. I have however tested this codepen which does all the important stuff, so if the code above doesn't work I've probably just made some spelling error. The concept should still work.
Happy coding!
Everything is global in javascript. It is calling the variable i which is set to 3 after your loop...if you set i to 1000 after the loop, then you would see each method call produce 1000 for i.
If you want to maintain state, then you should use objects. Have the object have a callback method that you assign to the click method.
You mentioned doing this for event bubbling...for stopping event bublling, you really do not need that, as it is built into the language. If you do want to prevent event bubbling, then you should use the stopPropagation() method of the event object passed to the callback.
function doStuff(event) {
//Do things
//stop bubbling
event.stopPropagation();
}

Jquery does not quite work inside for loop [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Javascript: closure of loop?
I have following code inside javascript:
for (var i=0; i < images_array.length; i++) {
$('#thumb_'+ i).live('click', function(){
$('#image_container_' + current_image_index).hide();
current_image_index = i;
alert(current_image_index);
$('#image_container_' + current_image_index).show();
});}
when I click on any thumb, i get images_array.length value. Does anyone know what is happenning?
You need to create a closure for the click handler function, like this:
for (var i=0; i < images_array.length; i++) {
$('#thumb_'+ i).live('click',
(function(i) {
return function(){
$('#image_container_' + current_image_index).hide();
current_image_index = i;
alert(current_image_index);
$('#image_container_' + current_image_index).show();
}
})(i)
);
}
The problem is that, without the closure, the variable is shared across every handler function -- it continues getting updated, which is why every handler ends up getting the array.length value. Using the closure creates a locally-scoped copy of the variable i.
Here's a demo that shows the difference:
Original
With closure
$.each(images_array,function(value,i) {
$('#thumb_'+ i).live('click', function(){
$('#image_container_' + current_image_index).hide();
current_image_index = i;
alert(current_image_index);
$('#image_container_' + current_image_index).show();
});}
As others have said, you need a closure. Now, you're already using jQuery so forget about for() and directly use $.each.

Categories

Resources