I'm creating an image carousel in javascript and I'm having problem while adding event listener, the basic code to have the image indicator is something like this:
Array.from(mCOMPLEX_ImgTags).forEach(
function(eachImageTag){
// mCOMPLEX_ImgPaths.push(eachImageTag.getAttribute("src"));
carouselIndicator = new COMPLEX_Img(carouselIndicatorProps, true);
carouselIndicator = carouselIndicator.make();
carouselIndicator.addEventListener("mouseenter", enterEvent=>{
CssHelper.assignStyleProp(carouselIndicator,{"background":"#ffa500"} );
});
carouselIndicator.addEventListener("mouseleave", enterEvent=>{
CssHelper.assignStyleProp(carouselIndicator,{"background":"#ff0000"});
});
mCOMPLEX_Container.appendChild(carouselIndicator);
});
now the indicators are fine and placed allright, but, the problem is that the variable carouselIndicator seems to be a reference to an object, and that reference is changed as it loops in javascript(i.e. the value of the reference is not captured whilst adding the listener, which I'd expect to). As a result, when I hover on any one of my 5 indicators, only the last one gets highlighted.
How can I save the reference that was supposed to be saved? i.e. the individual indicators have their references saved?
It appears that carouselIndicator is declared outside the scope of the closure in your forEach() loop, so this will only refer to a single location in memory. Because Javascript variables are assigned/copied by reference, each time you loop through you are just updating the value at that memory location. Meanwhile you are creating new event listeners, however they all point to the same thing.
Solution: declare your var inside the loop.
function(eachImageTag){
let carouselIndicator = new COMPLEX_Img(carouselIndicatorProps, true);
// ^^^
...
}
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);
}
This is probably very basic but I'm stalling ...
On page load, I need to save the html content of my element into a variable. I have other code in the page that will change the html content of the element. So I need to be able to revert the value back to it's default (what it was on page load). The issue is that my variable's value is being changed to most recent value.
How can I make the initial value I assign to the variable "stick"?
currentElementsHTML = $("#myDOMElement"),
currentElementsHTMLDefaultValue = currentElementsHTML.html()
... do stuff that changes currentElementsHTML
... revert to currentElementsHTMLDefaultValue whenever i need to
There are many ways you can store some data and make it available later, some of these require a knowledge of the way JavaScript's scope works - others just rely on jQuery methods.
the first things that come to mind
global variable
The bad way to do this would be to store the value as a global var:
function at_the_start(){
/// notice there is no var keyword, this means the variable will be global
global_html = $('element').html();
}
function later_on(){
$('element').html( global_html );
}
You shouldn't do this because your data will "pollute the global namespace" - which basically means that other code will easily be able to access your variable (and mess around with it) and that you could inadvertantly overwrite some other code's global data - especially if you use a rather general variable name.
local variable kept in scope
A better way to do this would be to use the power of JavaScript for your own ends, namely its scope abilities, there are some good points to read here -- What is the scope of variables in JavaScript?:
function my_code(){
var html = $('element').html();
/* Do stuff here */
$('element').html( html );
}
The above relies on a local variable and the fact that you must keep everything in the same function call. As it is most likely you will be relying on a mixture of user triggered events, you can't really use the above. This is because you will have many functions used in different locations and they can't all share the same local variable. Or can they?
The following is what I call a "global local" variable - completely most likely not its real name, but it describes things as I see them:
function my_code(){
/// this variable is local, due to the var keyword
/// but it will be accessible in both the functions below
var html_local = '';
var my_function_to_start = function(){
html_local = $('element').html();
}
var after_other_things_have_happened = function(){
$('element').html( html_local );
}
/// you can even apply these functions to say an event handler
/// and the variable will be remembered because it exists within
/// the "after_other_things_have_happened" function's scope.
$('another.element').click(after_other_things_have_happened);
}
The above works because JavaScript functions can always access variables defined in previous parent blocks / parent scopes or parent functions.
jQuery data
Considering you are using jQuery, jQuery offers a very simple method for storing arbitrary data and you don't need to know anything about scope or local and global vars. It's taken me a while to write this and so obviously by this time other posters have correctly stated that the following is a good idea - jQuery Data:
$('element').data( 'old_html', $('element').html() );
This can then be accessed any time after by using:
$('element').data( 'old_html' );
So...
$('element').html( $('element').data( 'old_html' ) );
Will put the value back - this is stored along with the element so whereever you can access $('element') you'll be able to get at the data assigned to it.
Some other less relevant ways (but still methods of data storage)
storing as a property of an object
Another useful ability sometimes, is that JavaScript treats nearly every datatype as an object. This means you can add properties to nearly anything. The following is actually quite possible if a little odd.
var a = new String('This is a string');
a.withAProperty = 'another string';
alert(a);
alert(a.withAProperty);
I occasionally use this to create pseudo static properties on functions, like so:
var my_function = function(){
if ( ! my_function.staticProp ) {
my_function.staticProp = 'abc';
}
/* use my_function.staticProp for something here */
}
var another_function(){
/* you can also access my_function.staticProp here
but only after my_function has been called once */
}
/* and my_function.staticProp here, but only
after my_function has been called once */
This almost has the same affect of using a global var (especially if you apply it to global functions) but means your value is stored on top of your functions namespace, cutting down the possibility of collisions with other code quite drastically. It does still mean outside code can influence the content of your var -- which can actually be a benefit depending on what you want to do.
storing content in the dom
Depending on what you wish to store, it can sometimes be of benefit to record that data in the DOM. The most obvious of these would be to write the data into a hidden input or hidden element. The benefit of the latter is that you can still navigate this data (using the likes of jQuery or document.getElementById) if it happens to take the form of markup information (as yours does). This can also be beneficial way of avoiding memory leaks caused by circular references - if you are dealing with large amounts of data - as long as you make sure to empty your variables involved in the transporting of the data.
$.ajax('request_html.php').done(function(data){
$('<div id="hidden_html" />').hide().html(data).appendTo('body');
data = null;
/// you only need mullify data if you were to have other
/// sub/child functions within this callback, mainly being wary
/// of closures - which are functions that are defined in a certain
/// scope chain, but are then returned or put to use outside of
/// that chain - i.e. like event listeners.
/// nullify vars and removing large properties is still good practice though.
});
Then when you want to retrieve:
$('#hidden_html').html();
And in the meantime between those two points you can obviously still traverse the data:
$('#hidden_html h1 > a[name=first]');
You associate the original HTML with the same DOM element, that way it won't disappear:
$("#myDOMElement").data("initial-html", $("#myDomElement").html());
something like that, but not tested yet:
$(function() {
$('#id').data('store', $('#id').html());
});
...
$('#id').html(data('store'));
Set it and forget it.
If you push the contents of .html() into a variable, it will stay there unless you do something with that variable to remove it:
var original = $("#foo").html(); // original HTML is now in 'origina'
This won't change unless you change it.
Storing data on the element with $.data()
It might be more advantageous for you to store it as data (using jQuery's .data method) on the element itself though:
var element = $("#foo");
element.data( "original", element.html() );
This way you can always access it at a later time:
console.log( element.data( "original" ) );
Record, Reset, and Restore Demo: http://jsfiddle.net/ft8M9/
Works on many items too
// Access all elements to restore
var restore = $(".restore");
// Save original HTML, and set new HTML
restore.each(function(i,o){
var that = $(this);
that.data("original", that.html())
.html("Changed " + i);
});
// After 2 seconds, restore original HTML, remove stored data
setTimeout(function(){
restore.each(function(i,o){
var that = $(this);
that.html( that.data("original") )
.removeData( "original" );
});
}, 2000);
Demo: http://jsfiddle.net/ft8M9/1/
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);
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