Javascript Array addEventListener - javascript

Interactive map with buttons in the shape of states, each button has the state abbreviation as an id, when a button/state is clicked I would like to fire the function "stateSelect" and send the state abbreviation with it so I know what's been pressed. Why doesn't the following work?
var stateList = new Array("AK","AL","AR","AS","AZ","CA","CO","CT","DC","DE","FL","GA","GU","HI","IA","ID",
"IL","IN","KS","KY","LA","MA","MD","ME","MH","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY",
"OH","OK","OR","PA","PR","PW","RI","SC","SD","TN","TX","UT","VA","VI","VT","WA","WI","WV","WY");
for (var i = 0; i < stateList.length; i++) {
document.getElementById(stateList[i]).addEventListener('mousedown', function() {stateSelect(stateList[i])}, false);
}
I obviously want to avoid 50 some lines of code but I'm not sure why this simple loop isn't working.

Because when the handler runs, it looks up the value of i, which is wherever it was after the loop finished.
You need to scope the i variable in a function:
function listenerForI( i ) {
document.getElementById(stateList[i]).addEventListener('mousedown', function() {stateSelect(stateList[i])}, false);
}
for (var i = 0; i < stateList.length; i++) {
listenerForI( i );
}
Now the i referenced by the handler will be the parameter to the listenerForI function that was invoked. As such, that i will reference the value that was passed in from the for loop.

You have a scoping issue. Javascript is not block-scoped; it is function-scoped. Basically, you must create a new function whenever you wish to create a new variable in a loop.
The most elegant way to do so is as follows:
stateList.map(function(abbrev){
$(abbrev).mousedown(function(){stateSelect(abbrev)});
});
If you are not using jQuery, merely replace $(abbrev).mousedown with document.getElementById(abbrev).addEventListener.
(Just to preempt the people who go "map isn't standard"; it is in the javascript ECMA-262 standard 5th edition which has support from all browser vendors. If one is paranoid about supporting older browsers, one can just $.map.)
Here is how one would do so using a for loop; it's a bit uglier but it demonstrates the necessity of creating new closures via functions:
for(var i=0; i<stateList.length; i++)
(function(i){
$(stateList[i]).mousedown(...);
})(i);
Like I said, a bit uglier than necessary; you could also do this which is slightly less ugly, but is basically the same thing:
function createListener(abbrev) {
$(abbrev).mousedown(...);
}
for(var i=0; i<stateList.length; i++)
createListener(stateList[i]);

Related

Trying to make sense of "this" in my javascript code (one thing works, the other doesn't)

I've been trying to learn javascript by refactoring some Jquery examples in a book into javascript. In the following code I add a click listener to a tab and make it change to active when the user clicks on the tab.
var tabs = document.querySelectorAll(".tabs a span");
var content = document.querySelectorAll("main .content li");
for (var tabNumber = 0; tabNumber <= 2; tabNumber++) {
tabs[tabNumber].addEventListener("click", function (event) {
for (var i = 0; i < tabs.length; i++) {
tabs[i].classList.remove("active");
}
tabs[tabNumber].classList.add("active");
for (var i = 0; i < content.length; i++) {
content[i].innerHTML = "";
}
event.preventDefault();
});
}
This returns an undefined error when I run it. However, I tried replacing tabs[tabNumber].classList.add("active") with this.classList.add("active") and it worked.
Why doesn't the previous code work? As far as I can see they are referring to the same thing, and tabs[tabNumber] should work since at that point in the code it is tabs[0].
If use this, I think it's better and a more polished solution. If you still want to use tabNumber, it's probably evaluating to 3 in every click callback, because it's the number after the last iteration, and you don't have a tabs[3] position.
So, you just have to make a closure of the tabNumber variable.
I guess other answers told you why tabs[tabNumber] does not work (because it comes from the score of the for loop and so, is always equal to the greater value of tabNumber).
That's why I would recommend using a .forEach loop. Careful though because it doesn't work on arrays of DOM nodes produced by document.querySelectorAll(), but you can use:
// ES6
Array.from(document.querySelectorAll('...'))
// ES5
[].slice.call(document.querySelectorAll('...'))
Anyway, I made a simplified working demo of your code.
Note that I save the currently active tab in a variable, to prevent another for loop. You could also do:
document.querySelector('.active').classList.remove('active')
But I like to reduce the amount of DOM reading.
Good luck for your apprentissage, re-writing some jQuery code into Vanilla JS seems like a good method, and you might acquire a far better comprehension of JavaScript.

Setting passed in values to event listeners in a loop doesn't work

This feels like a very basic question, but I can't seem to find an answer t it. I have an array of clickable svg rects that have id's "texture-1", "texture-2" etc. The textureInput array holds the getElementById's for those rects, that call a changeTexture() function on click. I want "1" to be passed into the changeTexture() function when "texture-1" is clicked on etc.
Manually assigning these values works: e.g.
textureInput[0].addEventListener('click', function(){changeTexture("0")}, false);
textureInput[1].addEventListener('click', function(){changeTexture("1")}, false);
But doing the same thing in a loop does not:
for (var i=0; i<maxTextures; i++){
textureInput[i].addEventListener('click', function(){changeTexture(i)}
};
Is it passing in the then current value of "i" at event time - which is undefined outside the loop?
You need to create a closure so that a new copy of the i variable is made, otherwise it will use the one that belongs to the for loop. Try this:
for (var i=0; i<maxTextures; i++){
textureInput[i].addEventListener('click', (function(i) {
return function () {
changeTexture(i);
};
})(i));
};
Use a closure. One simple way is wrap an IIFE around the code within the loop
for (var i=0; i<maxTextures; i++){
(function(i){
textureInput[i].addEventListener('click', function(){changeTexture(i);}
})(i);
};

Raphael.js - registering multiple events to element

my problem is that I need handle multiple events for rectangle. That sound simple,
for example this works
node.click(function(e){
click(); // this is function defined in same scope, it works ok
});
node.mouseout(function(e){
mouseout();
});
But, I want to automatize this, so it should looks like this:
var events = new Array("click", "mouseout");
for(var i in events){
node[events[i]](function(e){
events[i](); /*THIS is problem, no matter if it is click or mouseout
this always fires function with same name as last item
in events array (in this case mouseout)
*/
}
}
Do you have any idea why a how I should solve it?
Your handlers created in a loop are sharing a variable. By the time they are called, the variable is the last value in the loop.
You have to use a technique I call "freezing your closures" so that each handler gets a separate copy of the shared variable. In your case, the shared variable that changes is i
Your other problem is that you want to call your functions "click/mouseout" from a string, so you have to get a handle to the function, right now your code is attempting to call "hello"() which does not work
Your last problems (but not a bug yet) are that you shouldn't use the Array constructor and you shouldn't use a for in loop to iterate over arrays.
function createHandler(eventName) {
return function(e) {
window[eventName]();
}
}
var events = ["click", "mouseout"];
for(var i=0; i < events.length; i++){
node[events[i]](createHandler(events[i]));
}
The above example is easier to comprehend but you could use self invoking anonymous functions to do the same thing
var events = ["click", "mouseout"];
for(var i=0; i < events.length; i++){
node[events[i]]((function(eventName){
return function(e) {
window[eventName]();
};
})(events[i]));
}

Replacing repetitively occuring loops with eval in Javascript - good or bad?

I have a certain loop occurring several times in various functions in my code.
To illustrate with an example, it's pretty much along the lines of the following:
for (var i=0;i<= 5; i++) {
function1(function2(arr[i],i),$('div'+i));
$('span'+i).value = function3(arr[i]);
}
Where i is the loop counter of course. For the sake of reducing my code size and avoid repeating the loop declaration, I thought I should replace it with the following:
function loop(s) {
for (var i=0;i<= 5; i++) { eval(s); }
}
[...]
loop("function1(function2(arr[i],i),$('div'+i));$('span'+i).value = function3(arr[i]);");
Or should I? I've heard a lot about eval() slowing code execution and I'd like it to work as fast as a proper loop even in the Nintendo DSi browser, but I'd also like to cut down on code. What would you suggest?
Thank you in advance!
Why not just put the body of the loop into a function?
function doSomething(i, arr) {
function1(function2(arr[i],i), $('div'+i));
$('span'+i).value = function3(arr[i]);
}
and call it in the loop:
function loop() {
for (var i = 0; i <= 5; i++) { doSomething(i, arr); }
}
Gah!
This is a good question, but no, don't ever do that. Using eval in general is not recommended, as you won't see parse errors at load time, only at run time (harder to debug), it's harder to understand what's in scope when (harder to write), and you lose all your toolchain support (syntax highlight, script debugging).
Fortunately, since Javascript is basically a functional language, why not create a function that encapsulates what you want to do, and just call that?
function doMyThingNTimes(n, arr) {
for (var i=0;i <= n; i++) {
function1(function2(arr[i],i),$('div'+i));
$('span'+i).value = function3(arr[i]);
}
}
This is a dreadful idea.
It is inefficient
It is harder to debug
If you are concerned about bandwidth then use minification and HTTP compression.
Uh, no. eval should be treated as close to a last resort. JavaScript functions are First Class Objects so I would just declare whatever functions you need and pass them as one of the params.
Why not:
function loop(s) {
for (var i=0;i<= 5; i++) { s(i); }
}
loop(function4() {
function1(function2(arr[i],i),$('div'+i));$('span'+i).value = function3(arr[i]);
});

Dynamically Change HTML DOM event

I am trying to dynamically change an element's onClick event and I have something like the following:
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
Everything seems to work fine apart from the fact that the argument passed to 'existingFunction()' is the final value of i=4 each time it is called. Is there a way to bind a function to onclick that uses the value of i at the time of binding as opposed to what it seems to be doing at the moment and referencing the original i in the for-loop.
Also is is there a way of performing the same bind without having to create anonymous functions each time? so that I can directly reference 'existingFunction' in each onclick for performance reasons?
Cheers guys,
Yong
Change
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
to
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = createOneHandler(i);
}
function createOneHandler(number){
return function() {
existingFunction(number);
}
}
and it should work fine.
Working Demo
A good explanation is given here
JavaScript, time to grok closures
for the i being always 4, you have a scoping problem, I advise to read this. Scoping is are really important concept, so you have better to make sure to understand what's is going on.
a better code would be
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = existingFunction;
}
the onclick would pass an event has argument so you can know what element have been clicked
i.e.
function existingFunction(event){
// DO something here
}
you can read more about events there. IE does have the exact same event model as other browser so you would have to handle it.
Last bit, I advise you to use a JS framework(Jquery,ExtJS,DOJO,Prototype...) because it would simplify your task
the code you posted should work the way you intended, your problem with i=4 is elsewhere. edit: this is wrong, rageZ is right about the scoping problem.
re the other question: all you can do is offload the verbosity with
var f = function (i) { return function () { existingFunction(i); return false; } }
for (...) { document.getElementById(...).onclick = f(i); }
BTW, you should use something like jQuery for DOM manipulation (concise syntax), and perhaps Zeta (http://codex.sigpipe.cz/zeta/) for the function composition
var f = compose(false_, existingFunction);
for (...) { $(...).click(f(i));
Hooray! It's loop closures again! See 422784, 643542, 1552941 et al for some more discussion.
is there a way of performing the same bind without having to create anonymous functions each time?
Yes, in ECMAScript Fifth Edition you get function.bind:
for (var i = 1; i < 5; i++)
document.getElementById('element'+i).onclick= existingFunction.bind(window, i);
In the meantime since browsers don't yet generally support it you can monkey-patch an alternative implementation of bind (see the bottom of this comment for one such) built out of anonymous functions as a fallback.
Alternatively, assign the same event handler function to every element and just have it look at this.id to see which element number it is.

Categories

Resources