I have an HTML page with several forms (created dynamically), so I get them in JS into an array, and then I add an EventListener to each one, like this:
var forms = document.getElementsByTagName('form');
for(i=0; i < forms.length; i++) {
forms[i].addEventListener('change', function(){
checkAllFilled(i);
});
}
So in function checkAllFilled I do some stuff.
The problem is that if I have 7 forms (from forms[0] to forms[6]) and I work over forms[2], it is always called checkAllFilled(7), instead of calling checkAllFilled(2). It's like the variable i was set in the last value over the for loop.
How would I do this?
Thanks!
It's like the variable "i" was set in the last value over the for
loop.
This is exactly what's happening.
Think about it. Your event handler runs later, after the for-loop has been and gone. At that time, the variable has whatever value the loop left it with - hence, the event will always report the same, single value.
Instead, you need to bind the iterative value in via a closure. There's a few ways. One is via an immediately-executed function (IEF).
forms[i].addEventListener('change', (function(i) { return function(){
checkAllFilled(i);
}; })(i));
We pass the iterative value to our IEF as a function variable. This way, it's passed to the handler via a closure. It's worth reading up on these.
Another way is just to use let from ECMAScript 2015.
for(let i=0; i < forms.length; i++) {
forms[i].addEventListener('change', function(){
checkAllFilled(i);
});
}
You are always calling the last generated event listener, since you generate first these listeners (all with same name) and then call the last one of them.
The best solution here is to assign names for the functions handling the events based on the counter value i.
Use forEach()instead of a for loop:
var forms = document.getElementsByTagName('form');
[].slice.call(forms).forEach(function(form, i) {
form.addEventListener('change', function(){
checkAllFilled(i);
});
});
Related
I am learning web development and right now i'm working on vanilla javascript. I'm following a class on Udemy and everything is going well but there is a challenge where we are supposed to build a drumkit so everytime we click on a button it should trigger a function. So the solution the instructor is giving is using a for loop
var nbButtons = document.querySelectorAll(".drum").length;
for(i = 0; i<nbButtons; i++){
document.querySelectorAll(".drum")[i].addEventListener("click", handleClick);
function handleClick() {
alert("I got clicked!" + i);
}
}
So if I try to analyze the code I understand than :
We create a variable to keep track of the number of .drum elements (querySelectorAll(".drum") returns an array of all .drum elements and we get it's length.
We start a loop : We start listening for clicks on the .drum element whose number is equal to i and start the function handleClick.
So I tried it and it works but I don't understand why. the loop starts when the page is loaded which means i = nbButtons very quickly (i added console.log in the loop and it does) so logically it should listen only on clicks on the last element ?
I know i'm missing something but I don't know what.
Thanks in advance
We start listening for clicks on the .drum element whose number is equal to i and start the function handleClick.
This is the part you misunderstood.
You don't immediately start listening to the current element. This line registers the listener which then listens more or less in the background. So in your for loop you register an event listener for each array index. The event listener isn't overwritten on the next iteration. You're able to register multiple event listeners at once, even on the same element.
You can verify that in devtools:
More information on what addEventListener() does: https://www.w3schools.com/js/js_htmldom_eventlistener.asp
nbButtons is just the number of buttons, say 5 buttons.
Then you use a for loop to go through all the 5 buttons. The loop executes 5 times, and this number is temporarily stored in the i variable. So, using i you can also get to each button and add the event listener to that button.
You can log this with
for(i = 0; i<nbButtons; i++){
console.log(i);
console.log(document.querySelectorAll(".drum")[i])
}
There are several parts of your code that I would write differently (some pointed out in other answers), but the key problem is that you have a global variable i, which will already have the value nbButtons by the time any of the handleClick functions actually run - so it's no surprise that this is the value you see logged on each click.
To make sure the correct value of i is logged for each button, you have to ensure the function you add as the event handler actually uses the "current" value. Here is one way:
var nbButtons = document.querySelectorAll(".drum").length;
for(let i = 0; i<nbButtons; i++){
document.querySelectorAll(".drum")[i].addEventListener("click", function() { handleClick(i);} );
}
function handleClick(i) {
alert("I got clicked!" + i);
}
There are a few key differences from your faulty code. For one, I've pulled the definition of the handleClick function out of the loop - this doesn't actually matter here (function declarations are scoped to functions, not arbitrary blocks like loops), but is neater and matches better how the code actually behaves. More importantly, I've made it take a parameter, i, and made sure the correct i is passed in in each addEventListener call.
And most importantly of all, I've replaced your global variable i in the loop with a local one, declared with let. This ensures that when the button is clicked and the function - function() { handleClick(i);} is called, it sees not a global i whose value was long ago incremented past the intended value, but one scoped to the particular loop iteration, which therefore only ever had the one value. Note that let is crucial to make this work, using var would not (without some extra complications anyway) - see this classic question for more detail
What you have done in your for loop is search for a ".drum" using the queryselectorall function which I do not think is efficient - if you must loop with "for";
I think you should:
var nbButtons = document.querySelectorAll(".drum");
var nbButtonsLen= nbButtons.length;
for(i = 0; i<nbButtonsLen; i++){
nbButtons[i].addEventListener("click", handleClick);
}
function handleClick() {
alert("I got clicked!" + i);
}
I'm currently learning javascript from the following book 'JavaScript: The Good Parts - O'Reilly Media', which says the following:
It is important to understand that the inner function has access to the actual variables of the outer functions and not copies in order to avoid the following problem:
// BAD EXAMPLE
// Make a function that assigns event handler functions to an array of nodes the
wrong way.
// When you click on a node, an alert box is supposed to display the ordinal of the
node.
// But it always displays the number of nodes instead.
var add_the_handlers = function (nodes)
{
var i;
for (i = 0; i < nodes.length; i += 1)
{
nodes[i].onclick = function (e)
{
alert(i);
};
}
};
// END BAD EXAMPLE
Question: I don't understand what the problem is, if someone could give me a clear example with numbers and results that would greatly be appreciated.
The value of i changes every time the loop is incremented, and the event handlers will alert whatever the current value of i is. So if there are 8 elements, all of them will pop-up a value of 7 when the loop is done running.
The point of the example is that many people assume that when each handler is initially bound with the current value of i (e.g. 0, 1, 2, etc.) that it then doesn't change as i is incremented. This example demonstrates that this isn't the case, and that the event handler functions always have access to the current value of i, even after being bound.
The issue here is that the function you pass to onclick keeps a reference to the same i that is incremented at each loop turn. Thus, when the click is triggered, i has been incremented to match the number of elements.
I need to use bind the click event to an element in the DOM whilst being able to pass arguments on the fly, including the event object. My current script uses the following:
var counter = 1;
$(".dynamo_user_info .dynamo_awards").each(function(){
$(this).find("~ div a").each(function(){
var id = $(this).attr("rel").split("aid-")[1];
$(this).attr("id","dynamo_award-"+counter+"-"+id).bind('click',{c:counter,i:id},function(e){
e.returnValue = e.preventDefault && e.preventDefault() ? false : false;
dynamo.awards.tooltip.ini(c,i);
});
});
counter++;
});
Now, as you can see, counter increases on each iteration and id does not hold a static value. After each iteration, the final values are: counter = 4, id = 2. Now whenever one of these elements is clicked, it preventDefaults as appropriate, then runs dynamo.awards.tooltip.ini(c,i);.
However, this does not work as intended. Instead of it running:
dynamo.awards.tooltip.ini(1,1);
dynamo.awards.tooltip.ini(2,6);
etc (for example), it instead runs:
dynamo.awards.tooltip.ini(4,2);
dynamo.awards.tooltip.ini(4,2);
i.e it uses the last stored values for counter and id. My guess is it does this due to how bind works, passing the parameters when the element is clicked, not before. I'm not sure how to fix this, unless I can trick the scope of the function in some way?
The arguments are accessible through the event.data object , like
dynamo.awards.tooltip.ini(e.data.c, e.data.i);
http://api.jquery.com/bind/#passing-event-data
I have the following attached to a div element, which detects when a user right clicks on a div and calls the function theFunction.
$('#box_'+i).bind('mousedown',function(e){if(e.which==3)theFunction(e,i)});
The code is run inside a loop (where the i variable that is incremented), as there are multiple divs that need right click functionality on the page. However when theFunction is called it always passes the last value of i instead of the value that it was when the bind() was done. Is there a way to make it constant so it wont change i for the previous divs as the loop is run?
You'll have to capture the value of i in a local scope, inside of your loop:
// Start your loop
(function(i){
$('#box_'+i).bind('mousedown', function(e){
if (e.which==3) theFunction(e,i)
});
})(i);
// end your loop
The closure solutions are all good (they work just fine), but I thought I'd point out another way to do it. You can also access the object that triggered the event and pull the number off the id of the object. That would work something like this:
$('#box_'+i).bind('mousedown',function(e){
var matches = this.id.match(/_(\d+)$/);
var num = parseInt(matches[1], 10);
if(e.which == 3) theFunction(e, num);
});
Or if you really want to be object oriented (and use a neat capability of jQuery), you can save the index value as a separate data item associated with the object and retrieve it from there later.
$('#box_'+i).data("index", i).bind('mousedown',function(e){
if(e.which == 3) theFunction(e, $(this).data("index"));
});
Working demo of the second method here: http://jsfiddle.net/jfriend00/wVDHw/
for(var i=0;i<100;i++)
(function(i){
$('#box_'+i).bind('mousedown',function(e){
if(e.which==3)
theFunction(e,i);
});
})(i);
I'm generating an unordered list through javascript (using jQuery). Each listitem must receive its own event listener for the 'click'-event. However, I'm having trouble getting the right callback attached to the right item. A (stripped) code sample might clear things up a bit:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
Actually, more is going on in this iteration, but I didn't think it was very relevant to the question. In any case, what's happening is that the callback function seems to be referenced rather than stored (& copied). End result? When a user clicks any of the list items, it will always execute the action for the last class_id in the classes array, as it uses the function stored in callback at that specific point.
I found dirty workarounds (such as parsing the href attribute in an enclosed a element), but I was wondering whether there is a way to achieve my goals in a 'clean' way. If my approach is horrifying, please say so, as long as you tell me why :-) Thanks!
This is a classic "you need a closure" problem. Here's how it usually plays out.
Iterate over some values
Define/assign a function in that iteration that uses iterated variables
You learn that every function uses only values from the last iteration.
WTF?
Again, when you see this pattern, it should immediately make you think "closure"
Extending your example, here's how you'd put in a closure
for ( class_id in classes )
{
callback = function( cid )
{
return function()
{
$(this).selectClass( cid );
}
}( class_id );
li_item = jQuery('<li></li>').click(callback);
}
However, in this specific instance of jQuery, you shouldn't need a closure - but I have to ask about the nature of your variable classes - is that an object? Because you iterate over with a for-in loop, which suggest object. And for me it begs the question, why aren't you storing this in an array? Because if you were, your code could just be this.
jQuery('<li></li>').click(function()
{
$(this).addClass( classes.join( ' ' ) );
});
Your code:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
This is mostly ok, just one problem. The variable callback is global; so every time you loop, you are overwriting it. Put the var keyword in front of it to scope it locally and you should be fine.
EDIT for comments: It might not be global as you say, but it's outside the scope of the for-loop. So the variable is the same reference each time round the loop. Putting var in the loop scopes it to the loop, making a new reference each time.
This is a better cleaner way of doing what you want.
Add the class_id info onto the element using .data().
Then use .live() to add a click handler to all the new elements, this avoids having x * click functions.
for(class_id in classes) {
li_item = jQuery('<li></li>').data('class_id', class_id).addClass('someClass');
}
//setup click handler on new li's
$('li.someClass').live('click', myFunction )
function myFunction(){
//get class_id
var classId = $(this).data('class_id');
//do something
}
My javascript fu is pretty weak but as I understand it closures reference local variables on the stack (and that stack frame is passed around with the function, again, very sketchy). Your example indeed doesn't work because each function keeps a reference to the same variable. Try instead creating a different function that creates the closure i.e.:
function createClosure(class_id) {
callback = function() { this.selectClass(class_id) };
return callback;
}
and then:
for(class_id in classes) {
callback = createClosure(class_id);
li_item = jQuery('<li></li>').click(callback);
}
It's a bit of a kludge of course, there's probably better ways.
why can't you generate them all and then call something like
$(".li_class").click(function(){ this.whatever() };
EDIT:
If you need to add more classes, just create a string in your loop with all the class names and use that as your selector.
$(".li_class1, .li_class2, etc").click(function(){ this.whatever() };
Or you can attach the class_id to the .data() of those list items.
$("<li />").data("class_id", class_id).click(function(){
alert("This item has class_id "+$(this).data("class_id"));
});
Be careful, though: You're creating the callback function anew for every $("<li />") call. I'm not sure about JavaScript implementation details, but this might be memory expensive.
Instead, you could do
function listItemCallback(){
alert("This item has class_id "+$(this).data("class_id"));
}
$("<li />").data("class_id", class_id).click(listItemCallback);