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.
Related
This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Javascript infamous Loop issue? [duplicate]
(5 answers)
Closed 8 years ago.
I have this weird situation and I don't know why do the output is not what I expect. This is only a simple for-loop function. Can somebody explain me why this happens?
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).mouseenter(function(){
alert(i);
});
};
I have 2 divs in my html that has id="specialty_pm_<?php echo $countPM; ?>" and that div is inside a for loop function in php. foreach ($employee_info as $emp_info){ $countPM++; }
I expect that the alert on hover in the 1st div is '1' and the 2nd div is '2'. but when I hover the 1st div, it will alert '2'.
you should use JavaScript closure:
var pm = 2;
for (var i = 0; i < pm; i++) {
var func = (function(i){
return function(){
alert(i);
}
})(i);
$("#specialty_pm_"+i).mouseenter(func);
};
The point is in your code all the mouseenter functions use the same variable i, and after your loop ends, it has its last value which is 2. Using a scope chain, with nested functions and their closures in JavaScript, you can create safe scopes for your variables. Basically what nested functions do, is to provide a outer LexicalEnvironment for the inner function. You can find more information in this post:
Scope Chain in Javascript.
You alert can't wirks because i has only one instance.
Yoi can check variable i inside your div in this case.
try this:
$("#specialty_pm_"+i).mouseenter(function(){
var id = $(this).attr('id');
alert(id.substring(0,13));
});
The reason, as already mentioned, is that the scope of i is the same for both eventhandlers, and as such it will have the same value for both of them.
There is a couple of solution for this problem.
Solution 1: create a new scope via a immediate function
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).mouseenter(function(instance){
return function() { alert(instance); };
}(i));
};
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/
Solution 2: use jQuerys data method to store the value
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).data('instance',i).mouseenter(function(){
alert($(this).data('instance'));
});
};
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/1/
Solution 3: bind the instance number to the eventhandler
var pm = 2;
for (var i = 0; i < pm; i++) {
$("#specialty_pm_"+i).mouseenter(function(instance){
alert(instance);
}.bind(null,i));
};
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/2/
Solution 3 has a few caveats - this is being bound as null, and thus it can no longer be used as a reference to the dom element, like jQuery eventhandlers nomrally do. Also bind isn't supported by older browsers, but this can be mitigated by usinga polyfill, a good one can be found here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
Solution 4: Be smart and use a delegate instead of binding event handlers in a loop
$(document.body).on('mouseenter','.specialty_pm',function(){
alert($(this).data('id'));
});
You can see a fiddle of it in action here: http://jsfiddle.net/LP6ZQ/4/
Solution 4 is the "right way" to do it, but it will require you to change the way you build your markup
You could get rid of the for loop in your JavaScript and use jQuery's Starts With Selector to select ALL elements whose id begins with 'specialty_pm_'
$("[id^='specialty_pm_']").on("mouseenter",function(){
var id = $(this).attr('id');
alert(id.substring(13));
});
E.G: http://jsfiddle.net/6bTJ3/
Alternatively you could add a class to each of these specialty_pm_[n] items to make it even simpler to select them in jQuery.
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]));
}
How do I maintain access to the i variable inside my for loop below? I'm trying to learn, not just get the answer, so a bit of explanation would be very helpful. Thank you!
var el,
len = statesPolyStrings.length;
for (var i = 0; i < len; i++) {
el = document.getElementById(statesPolyStrings[i]);
google.maps.event.addDomListener(el, 'mouseover', function() {
$("#"+statesPolyStrings[i]).addClass("highlight");
statesPolyObjects[i].setOptions({ strokeWeight: '2' });
});
}
All of your callbacks share the same i variable.
When the event handler actually runs, i is after the end of the array.
You need to wrap the loop body in a self-invoking function that takes i as a parameter.
This way, each iteration will get its own, unchanging, i variable.
For example:
for (var i = 0; i < statesPolyStrings.length; i++) {
(function(i) {
...
})(i);
}
for (var i = 0; i < statesPolyStrings.length; i++) {
(function(i){
google.maps.event.addDomListener(document.getElementById(statesPolyStrings[i]), 'mouseover', function() {
$("#"+statesPolyStrings[i]).addClass("highlight");
statesPolyObjects[i].setOptions({ strokeWeight: '2' });
});
})(i)
}
The trick with self-invoking functions works fine: it creates a new scope (maybe google for 'scope in functions javascript') and therefore handles i as a different variable and delivers the right value to your event listener callback function.
But you actually don't need to find your element again with jQuery as you already assigned an event listener to it and inside your function you have a reference to your element with this.
And as you're using jQuery anyhow, it's then easy to find the correct index (your i) of statesPolyObjects with $.inArray() passing the id of your element and the statesPolyStrings array (assuming you're dealing with unique IDs. If not, $("#"+statesPolyStrings[i]) would also fail, as it takes the first it finds).
var el;
for (var i = 0, len = statesPolyStrings.length; i < len; i++) {
el = document.getElementById(statesPolyStrings[i]);
google.maps.event.addDomListener(el, 'mouseover', function(event) {
$(this).addClass("highlight");
statesPolyObjects[$.inArray(this.id, statesPolyStrings)].
setOptions({ strokeWeight: '2' });
});
}
If you still want to stick with the self-invoking function you should anyhow change the following line:
("#"+statesPolyStrings[i]).addClass("highlight");
to
$(this).addClass("highlight");
If you're not familiar enough with this and events you might want to read this article:
http://www.sitepoint.com/javascript-this-event-handlers/
You might have noticed that I also wrote the argument event inside the anonymous callback function. Try to console.log this event you get delivered for free with any event listener callback function and explore all the other things you have access to. For example, you can find the actual element you clicked on with event.target (as the actual mouseover might have happened to a child of your element). So:
google.maps.event.addDomListener(el, 'mouseover', function(event) {
console.log(event);
...
and open the console of your browser to see what event delivers...
Be aware though that google.maps.event.addDomListener passes something different then document.body.addEventListener and there is also a difference between browsers. jQuery.on() for example also delivers some different things in the event object, but there you can at least count on the same data in all browsers.
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]);
I was toying (read: learning) around with Javascript and came across something to my understanding, seems very odd. It has to do with closures and a reference that seems to 'loose' its importance to the browser.
The browser I am using is Chromium 5.0.307.7.
Anyway, here's some code:
HTMLElement.prototype.writeInSteps = function() {
var i = 0;
var elem = this;
var args = arguments;
function step() {
elem.innerHTML += args[i];
if(i < args.length) {
i += 1;
} else {
elem.innerHTML = "";
i = 0;
}
setTimeout(step, 500);
}
step();
}
What happens here is that the first argument gets written to the correct HTMLElement, but all the ones after does not. What seems to happen is that after the first argument, the following arguments are written to some other element that is now being referenced by 'elem'.
I should also mention that, this only seems to happen when I write something directly after calling this function, like this:
div.writeInSteps("This", " is", " not", " working");
$id("body").innerHTML += "Doh!";
If I refrain from writing anything after calling this function, it seems to work ok.
If I instead change the above code to:
HTMLElement.prototype.writeInSteps = function() {
var i = 0;
var e = this.id;
var args = arguments;
function step() {
var elem = $id(e);
elem.innerHTML += args[i];
if(i < args.length) {
i += 1;
} else {
elem.innerHTML = "";
i = 0;
}
setTimeout(step, 500);
}
step();
}
Everything is dandy. My question is, what's really happening behind the scenes in the first version?
EDIT: Updated with requested details about "...write something directly after..." and browser usage as requested by ntownsend. Bryan Matthews, I'm not sure how to provide a test page without making this question overly cluttered though.
I suspect this is a DOM issue, not a JavaScript issue.
My guess is that something's mutating an ancestor of the element to which you're trying to write in steps. For example, if innerHTML of the element's parent is set (even to the exact same string, I think), the element reference you have will be to an element that's no longer in the DOM. Re-getting the element by ID each time would work around that problem.
If you're replacing the innerHTML of an ancestor of elem (the body element, as per your example), then elem no longer exists. When step is after the original elem is destroyed, what elem is referencing is going to be something else.
The correct thing for the browser to do should probably be to remove the elem reference, but it doesn't look like it's doing that.
my guess is, execution of setTimeout(step, 500); callback has very little idea who this is - whether you're calling current element's step or maybe HTMLElement.prototype.writeInSteps.step() ?
Try your second code on two different elements simultaneously, (so that second writeInSteps comes before first timeout). I'm pretty sure it won't do quite what you expect.
Of course Jeff can be right too. innerHTML is read-only by specification, and writing to it may or may not rebuild the whole tree (and kill all the references).