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
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);
}
When dynamically creating an element of type select, there are two problems when setting the onclick method:
It is impossible to simply set the onclick with element.onclick="updateInput(this.articleIndex)";
This results in a final HTML tag where no onclick is shown at all.
When set by e.setAttribute("onclick","updateInput(this.articleIndex)");, it does appear in the final HTML. And the updateInput method does get called.
However the functionality seems to be broken, as the argument always evaluates to undefined
Here a simple example of my problems:
var selectElem = document.createElement("select");
selElem.id="articleSelector_"+this.articleIndex;
console.log("the index of the article is " + this.articleIndex);
selElem.setAttribute("onclick","updateInput(this.articleIndex);");
//selElem.onclick="updateInput(this.articleIndex)"; //this does not work
The log shows the correct number. Inside the updateInput method, the argument is of value undefined instead of the number previously shown in the log.
Try attaching handlers with pure Javascript, and not with HTML, without onclick = "... (which is as bad as eval).
The this in your script refers to the calling context of the function - what is it?
You might want:
element.addEventListener('click', () => {
updateInput(this.articleIndex);
});
(arrow functions retain the this of their surrounding scope)
it is impossible to simply set the onclick with element.onclick="updateInput(this.articleIndex)";
What that code does is it assigns the string "updateInput(this.articleIndex)" to the onclick which makes no sense and certainly not what you want.
Even if you remove the quotes:
element.onclick = updateInput(this.articleIndex);
It is still incorrect because it assigns the result of the updateInput() function to the onclick which is again not what you want.
You need to assign a function name to the onclick like this:
element.onclick = updateInput;
However, this doesn't allow you to pass a parameter as you wish. To do so, you need to use an anonymous function:
element.onclick = function() {
updateInput(this.articleIndex)
};
When set by e.setAttribute("onclick","updateInput(this.articleIndex)");, it does appear in the final HTML. And the updateInput method does get called.
This works because it sets the attribute onclick and it is a string type, so everything is correct. It is equivalent to using the anonymous function above. The only difference is this, which in this case refers to the element itself, while in the above code it depends on the context that the code appears in. That's why in this case the argument always evaluates to undefined because the select element doesn't have an articleIndex property.
The problem is the value of the context this when that element is clicked, the context this is not available anymore at that moment.
You have two ways to solve this problem:
You can use the function addEventListener to bind the event click, and bind the function/handler with the desired context this:
The function bind binds a specific context to a function.
selElem.addEventListener('click', updateInput.bind(this));
function updateInput() {
console.log(this.articleIndex);
}
As you need a specific value, you can use data attributes. That way, you don't need to worry about the context this.
selElem.dataset.articleIndex = this.articleIndex;
selElem.addEventListener('click', function() {
updateInput(this.dataset.articleIndex); // Here you can get that value.
});
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);
});
});
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);
The Objective
I want to dynamically assign event handlers to some divs on pages throughout a site.
My Method
Im using jQuery to bind anonymous functions as handlers for selected div events.
The Problem
The code iterates an array of div names and associated urls. The div name is used to set the binding target i.e. attach this event handler to this div event.
While the event handlers are successfully bound to each of the div events, the actions triggered by those event handlers only ever target the last item in the array.
So the idea is that if the user mouses over a given div, it should run a slide-out animation for that div. But instead, mousing over div1 (rangeTabAll) triggers a slide-out animation for div4 (rangeTabThm). The same is true for divs 2, 3, etc. The order is unimportant. Change the array elements around and events will always target the last element in the array, div4.
My Code - (Uses jQuery)
var curTab, curDiv;
var inlineRangeNavUrls=[['rangeTabAll','range_all.html'],['rangeTabRem','range_remedial.html'],
['rangeTabGym','range_gym.html'],['rangeTabThm','range_thermal.html']];
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
curTab=(inlineRangeNavUrls[i][0]).toString();
curDiv='#' + curTab;
if ($(curDiv).length)
{
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
$(curDiv).bind("mouseout", function(){showHideRangeSlidingTabs(curTab, false);} );
}
}
My Theory
I'm either not seeing a blindingly obvious syntax error or its a pass by reference problem.
Initially i had the following statement to set the value of curTab:
curTab=inlineRangeNavUrls[i][0];
So when the problem occured i figured that as i changed (via for loop iteration) the reference to curTab, i was in fact changing the reference for all previous anonymous function event handlers to the new curTab value as well.... which is why event handlers always targeted the last div.
So what i really needed to do was pass the curTab value to the anonymous function event handlers not the curTab object reference.
So i thought:
curTab=(inlineRangeNavUrls[i][0]).toString();
would fix the problem, but it doesn't. Same deal. So clearly im missing some key, and probably very basic, knowledge regarding the problem. Thanks.
You need to create a new variable on each pass through the loop, so that it'll get captured in the closures you're creating for the event handlers.
However, merely moving the variable declaration into the loop won't accomplish this, because JavaScript doesn't introduce a new scope for arbitrary blocks.
One easy way to force the introduction of a new scope is to use another anonymous function:
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
curDiv='#' + inlineRangeNavUrls[i][1];
if ($(curDiv).length)
{
(function(curTab)
{
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
$(curDiv).bind("mouseout", function(){showHideRangeSlidingTabs(curTab, false);} );
})(inlineRangeNavUrls[i][0]); // pass as argument to anonymous function - this will introduce a new scope
}
}
As Jason suggests, you can actually clean this up quite a bit using jQuery's built-in hover() function:
for (var i=0;i<inlineRangeNavUrls.length;i++)
{
(function(curTab) // introduce a new scope
{
$('#' + inlineRangeNavUrls[i][1])
.hover(
function(){showHideRangeSlidingTabs(curTab, true);},
function(){showHideRangeSlidingTabs(curTab, false);}
);
// establish per-loop variable by passsing as argument to anonymous function
})(inlineRangeNavUrls[i][0]);
}
what's going on here is that your anonmymous functions are forming a closure, and taking their outer scope with them. That means that when you reference curTab inside your anomymous function, when the event handler runs that function, it's going to look up the current value of curTab in your outer scope. That will be whatever you last assigned to curTab. (not what was assigned at the time you binded the function)
what you need to do is change this:
$(curDiv).bind("mouseover", function(){showHideRangeSlidingTabs(curTab, true);} );
to this:
$(curDiv).bind("mouseover",
(function (mylocalvariable) {
return function(){
showHideRangeSlidingTabs(mylocalvariable, true);
}
})(curTab)
);
this will copy the value of curTab into the scope of the outer function, which the inner function will take with it. This copying happens at the same time that you're binding the inner function to the event handler, so "mylocalvariable" reflects the value of curTab at that time. Then next time around the loop, a new outer function, with a new scope will be created, and the next value of curTab copied into it.
shog9's answer accomplishes basically the same thing, but his code is a little more austere.
it's kinda complicated, but it makes sense if you think about it. Closures are weird.
edit: oops, forgot to return the inner function. Fixed.
I think you're making this more complicated than it needs to be. If all you're doing is assigning a sliding effect on mouseover/out then try the hover effect with jquery.
$("#mytab").hover(function(){
$(this).next("div").slideDown("fast");},
function(){
$(this).next("div").slideUp("fast");
});
If you posted your full HTML I could tell you exactly how to do it :)
You can put your variable's value into a non existing tag, and later you can read them from there. This snippet is part of a loop body:
s = introduction.introductions[page * 6 + i][0]; //The variables content
$('#intro_img_'+i).attr('tag' , s); //Store them in a tag named tag
$('#intro_img_'+i).click( function() {introduction.selectTemplate(this, $(this).attr('tag'));} ); //retrieve the stored data