How to properly pass argument in loop to multiple event handlers? [duplicate] - javascript

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Javascript closure inside loops - simple practical example
I add event handlers to multiple hrefs on my website with JS like this:
function addButtonListener(){
var buttons = document.getElementsByClassName("selLink");
for (var i = 0; i < buttons.length; i++)
{
button.addEventListener('click',function() { addTosel(i); },true);
}
}
}
But unfortunately to addTosel is passed the last i not the i from the loop. How to pass i accordingly to the object being processed in this moment?

You need to create a closure:
function addButtonListener(){
var buttons = document.getElementsByClassName("selLink");
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click', function(index) {
return function () {
addTosel(index);
};
}(i), true);
}
}
This way the scope of the handler is bound to the proper context of i.
See this article for more information on this subject.

You need to bind the i variable to the function when its declared. like so
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click',(function() { addTosel(this); }).bind(i) ,true);
}
Note: I just wrote the code from memory so it may not be perfect, but it is the sulution you're needing, for reference as to the proper way, ie with cross browser shims etc look at:
https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind

If you're going to take the .bind approach, do it like this.
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click', addTosel.bind(null, i), true);
}
This makes a new function with null bound as the this value since your function doesn't seem to need it, and the current i bound as the first argument.
Or make your own binder function
var _slice = Array.prototype.slice;
function _binder(func, ctx /*, arg1, argn */) {
var bound_args = _slice.call(arguments, 2);
return function() {
return func.apply(ctx, bound_args.concat(_slice.call(arguments)));
}
}
And then do this.
for (var i = 0; i < buttons.length; i++) {
button.addEventListener('click', _binder(addTosel, null, i), true);
}

Related

How to fix scope issues when using an onclick event in a for loop [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 3 years ago.
I am curretly experiencing difficulties implementing an onclick event within a for loop. Instead of alerting the respective value it always returns undefined (presumably a scope problem, because the iteration itself works fine)
Until now I tried to pass on the i variable to the onclick function; however, with little success
for (var i = 0; i < timeSpanLength; i++) {
// creating the wrap for the month
var month = document.createElement("div");
month.className = 'month_element';
var reference_month = document.createElement("span");
reference_month.innerHTML = time_span[i];
//onclick event
reference_month.onclick = function(i) {
var month_beginning = signup_date;
var month_end = time_span[i];
alert(month_end);
//searchForData(month_beginning, month_end);
};
//append to container
month.appendChild(reference_month);
document.getElementById('time_container').appendChild(month);
}
The expected outcome is to trigger an alert which displays the same month which is displayed in the span element above. I need the variable to pass it on to another function.
Any help is highly appreciated since I am beginner in javascript.
for (var i = 0; i < timeSpanLength; i++) {
(function (index) {
// creating the wrap for the month
var month = document.createElement("div");
month.className = 'month_element';
var reference_month = document.createElement("span");
reference_month.innerHTML = time_span[index];
//onclick event
reference_month.onclick = function() {
var month_beginning = signup_date;
var month_end = time_span[index];
alert(month_end);
//searchForData(month_beginning, month_end);
};
//append to container
month.appendChild(reference_month);
document.getElementById('time_container').appendChild(month);
})(i);
}
This callback function handler is forming a closure with respect to the outer scope. Also var has a function scope, so in essence the block of code can be re-written as:
var i;
for (i = 0; i < timeSpanLength; i++) {
...
//onclick event
reference_month.onclick = function(i) {
var month_beginning = signup_date;
var month_end = time_span[i];
alert(month_end);
//searchForData(month_beginning, month_end);
};
...
}
So the var i is hoisted to the top and when the loop completes the value of i is timeSpanLength.length and this is what you use to access time_span[i] and that returns undefined.
Since with var the binding remains the same, the handlers registered will be referring the last value of i in the loop.
So you either need to use let in the for-loop:
for (let i = 0; i < timeSpanLength; i++) { ... }
Or an IIFE which forms a new scope bound to each new value of i from the loop:
for (var i = 0; i < timeSpanLength; i++) {
(function(i){
reference_month.onclick = function(i) {
var month_beginning = signup_date;
var month_end = time_span[i];
alert(month_end);
//searchForData(month_beginning, month_end);
};
})(i)
}

How to reference a looped element inside a jQuery event [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
Script:
for (var i = 0; i < products.length; i++) {
for (var j = 0; j < products.length; j++) {
$(document).on('change', $(products[i][j].checkbox) , function () {
products[i][j].checked ? products[i][j].checked = false : products[i][j].checked = true;
};
}
}
How I cant get products[i][j] in my event function?
Use an anonymous function that's invoked immediately to create a new scope.
This way the anonymous function of the change event becomes a closure.
for (var i = 0; i < products.length; i++) {
for (var j = 0; j < products.length; j++) {
//anonymous function (outer function)
(function() {
var product = products[i][j];
//now the anonymous function of the change event will be called
//within this new scope, with each unique product in it
$(document).on('change', function () {
//this is function is now a closure with scope of the outer function
product.checked ? product.checked = false : product.checked = true;
};
})();
}
}
The scope of the product variable will fall inside the anonymous function closure and each change event will have a unique product value associated.
Fiddle: http://jsfiddle.net/mattdlockyer/t2h50aun/1/
Closures: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
Immediately Invoked Functions: http://en.wikipedia.org/wiki/Immediately-invoked_function_expression
There seems to be another issue with your code, why are you checking document for change?

Canot get the href value

Hi I need to valid the href is empty or not on my page using javascript. I searched the site and found some example, but it didn't worked for me. I must miss something that I didn't notice. Would someone point me the good direction and my mistake. I got the error" Unable to get property 'getattribute' of undefined or null reference. The <a> element is like that <a name="playback" href=""> on html file.
Thanks in advance.
There is my code which is run on load event:
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++)
{
anchors[i].onclick = function() {
if (anchors == null) {
alert('null');
}
else {
var link = anchors[i].getAttribute("href");
//var link= anchors[i].attributes['href'] this line doesn't work too.
}
}
}
}
In your code, the call to getAttribute is inside a closure (that is, a function defined "inline" without a name) that is assigned to the onlick event handler of the link. Therefore that code isn't execxuted right away - it doesn't run before the onclick handler triggers.
When the onclick header triggers, two things are passed to the callback function: the element on which the event was triggered is assigned to the this variable of the functions context - and the event itself is passed as first parameter. anchors however is undefined in the scope of that callback.
So, use either of those:
anchors[i].onclick = function () {
var link = this.getAtrribute("href");
}
 
anchors[i].onclick = function (event) {
var link = event.target.getAttribute("href");
}
You have got a scope problem.
The following code will output 3:
for (var i = 0; i < 3; i++) {
}
console.log(i); // 3
Similar to the example above your onclick is fired after the loop is done.
So i in your example would equal to anchors.length.
And anchors[anchors.length] === undefined.
To solve this problem you have to create a new scope.
For example you could use an Immediately-Invoked Function Expression (IIFE):
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++)
{
(function(j){
anchors[j].onclick = function() {
if (anchors == null) {
alert('null');
}
else {
var link = anchors[j].getAttribute("href");
}
}
}
}(i));
}
You need to use closure if you want to do it this way since you are using the shared i variable which would have been having last value of iteration when your handler runs on click. But since you are looking at that particular anchor, try binding it with bind an event listener and access it using this.href:
You can use addEventListener and for older browser support attachEvent
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++) {
anchors[i].addEventListener('click', function () {
var link = this.getAttribute("href");
})
};
Demo
Or :
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++) {
anchors[i].onclick = getHandler(i);
}
function getHandler(i) {
return function () { //Now each of your handler has its own `i`
var link = anchors[i].getAttribute("href");
}
}
Demo
I have never seen getAttribute before so I performed some tests on it. It turns out that href and getAttribute("href") are quite different. Namely href is the absolute url and getAttribute("href") is the relative url to the page. See this fiddle.
The problem with your code is that the var is captured in the closure of onclick and when the onclick function runs the value will of i will be anchors.length.
Solution, Scratch that use the code from Johannes H. His is better
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++) {
(function () {
var current = anchors[i]; //Capture the anchor element
current.onclick = function() {
var link = current.getAttribute("href");
};
} ());
}
See this w3 schools page for how to get the href attribute from anchor tags.
http://www.w3schools.com/jsref/prop_anchor_href.asp
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++)
{
anchors[i].onclick = function() {
if (anchors == null) {
alert('null');
}
else {
var link = this.href;
}
}
}

addEventListener on NodeList [duplicate]

This question already has answers here:
Adding click event listener to elements with the same class
(5 answers)
Closed 8 months ago.
Does NodeList support addEventListener. If not what is the best way to add EventListener to all the nodes of the NodeList. Currently I am using the code snippet as show below, is there a better way to do this.
var ar_coins = document.getElementsByClassName('coins');
for(var xx=0;xx < ar_coins.length;xx++)
{
ar_coins.item(xx).addEventListener('dragstart',handleDragStart,false);
}
There is no way to do it without looping through every element. You could, of course, write a function to do it for you.
function addEventListenerList(list, event, fn) {
for (var i = 0, len = list.length; i < len; i++) {
list[i].addEventListener(event, fn, false);
}
}
var ar_coins = document.getElementsByClassName('coins');
addEventListenerList(ar_coins, 'dragstart', handleDragStart);
or a more specialized version:
function addEventListenerByClass(className, event, fn) {
var list = document.getElementsByClassName(className);
for (var i = 0, len = list.length; i < len; i++) {
list[i].addEventListener(event, fn, false);
}
}
addEventListenerByClass('coins', 'dragstart', handleDragStart);
And, though you didn't ask about jQuery, this is the kind of stuff that jQuery is particularly good at:
$('.coins').on('dragstart', handleDragStart);
The best I could come up with was this:
const $coins = document.querySelectorAll('.coins')
$coins.forEach($coin => $coin.addEventListener('dragstart', handleDragStart));
Note that this uses ES6 features, so please make sure to transpile it first!
There actually is a way to do this without a loop:
[].forEach.call(nodeList,function(e){e.addEventListener('click',callback,false)})
And this way is used in one of my one-liner helper libraries - nanoQuery.
The simplest example is to add this functionality to NodeList
NodeList.prototype.addEventListener = function (event_name, callback, useCapture)
{
for (var i = 0; i < this.length; i++)
{
this[i].addEventListener(event_name, callback, useCapture);
}
};
Now you can do:
document.querySelectorAll(".my-button").addEventListener("click", function ()
{
alert("Hi");
});
In the same way, you can do a forEach loop
NodeList.prototype.forEach = function (callback)
{
for (var i = 0; i < this.length; i++)
{
callback(this[i], i);
}
};
Using:
document.querySelectorAll(".buttons").forEach(function (element, id)
{
input.addEventListener("change", function ()
{
alert("button: " + id);
});
});
EDIT : note that NodeList.prototype.forEach has existed ever since november 2016 in FF. No IE support though
in es6, you can do a array from nodelist, using Array.from, e.g.
ar_coins = document.getElementsByClassName('coins');
Array
.from(ar_coins)
.forEach(addEvent)
function addEvent(element) {
element.addEventListener('click', callback)
}
or just use arrow functions
Array
.from(ar_coins)
.forEach(element => element.addEventListener('click', callback))
Another solution is to use event delegation. You just use addEventListener to the closest parent of the .coins elements and use event.target in the callback to check if the click was really on an element with the class "coins".
I suppose another option would be to define addEventListener on NodeList using Object.defineProperty. That way you can treat the NodeList as you would a single Node.
As an example, I created a jsfiddle here: http://jsfiddle.net/2LQbe/
The key point is this:
Object.defineProperty(NodeList.prototype, "addEventListener", {
value: function (event, callback, useCapture) {
useCapture = ( !! useCapture) | false;
for (var i = 0; i < this.length; ++i) {
if (this[i] instanceof Node) {
this[i].addEventListener(event, callback, useCapture);
}
}
return this;
}
});
You could also use prototyping
NodeList.prototype.addEventListener = function (type, callback) {
this.forEach(function (node) {
node.addEventListener(type, callback);
});
};

Scope troubles in Javascript when passing an anonymous function to a named function with a local variable

Sorry about the title - I couldn't figure out a way to phrase it.
Here's the scenario:
I have a function that builds a element:
buildSelect(id,cbFunc,...)
Inside buildSelect it does this:
select.attachEvent('onchange',cbFunc);
I also have an array that goes:
var xs = ['x1','x2','x3'...];
Given all of these, I have some code that does this:
for(var i = 0; i < xs.length; i++)
{
buildSelect(blah,function(){ CallBack(xs[i],...) },...);
}
The issue is that when onchange gets fired on one of those selects it correctly goes to CallBack() but the first parameter is incorrect. For example if I change the third select I expect CallBack() to be called with xs[2] instead I get some varying things like xs[3] or something else.
If I modify it slightly to this:
for(var i = 0; i < xs.length; i++)
{
var xm = xs[i];
buildSelect(blah,function(){ CallBack(xm,...) },...);
}
I'm still getting incorrect values in CallBack(). Something tells me this is scope/closure related but I can't seem to figure out what.
I simply want the first select to call CallBack for onchange with the first parameter as xs[0], the second select with xs[1] and so on. What could I be doing wrong here?
I should clarify that xs is a global variable.
Thanks
You need to capture that xm value by closing around it in its own scope.
To do this requires a separate function call:
buildCallback( curr_xm ) {
// this function will refer to the `xm` member passed in
return function(){ CallBack(curr_xm,...) },...);
}
for(var i = 0; i < xs.length; i++)
{
var xm = xs[ i ];
buildSelect(blah,buildCallback( xm ),...);
}
Now the xm that the callback refers to is the one that you passed to buildCallback.
If you have other uses for i that need to be retained, you could send that instead:
buildCallback( curr_i ) {
// this function will refer to the `i` value passed in
return function(){ CallBack( xs[ curr_i ],...) },...);
}
for(var i = 0; i < xs.length; i++)
{
buildSelect(blah,buildCallback( i ),...);
}
The problem is indeed scope-related -- JavaScript has only function scope, not block scope or loop scope. There is only a single instance of the variables i and xm, and the value of these variables changes as the loop progresses. When the loop is done, you're left with only the last value that they held. Your anonymous functions capture the variables themselves, not their values.
To capture the actual value of a variable, you need another function where you can capture the local variable:
function makeCallback(value) {
return function() { CallBack(value, ...) };
}
Each call to makeCallback gets a new instance of the value variable and if you capture this variable, you essentially capture the value:
for(var i = 0; i < xs.length; i++)
{
buildSelect(blah,makeCallback(xs[i]),...);
}
Yes, I think a closure would help:
for(var i = 0, l = xs.length; i < l; i++)
{
buildSelect(
blah,
function(xm){
return function(){
CallBack(xm,...)
};
}(xs[i]),
...
);
}
Edit: I also optimised your for loop slightly.
Edit: I guess I'll add an explanation. What you're doing is creating an anonymous function which takes one argument (xm) and calling the function straight away (with the parenthesis right after). This anonymous function must also return your original function as an argument of buildSelect().
Apparently there is a new let keyword that does what you want:
for(var i = 0; i < xs.length; i++)
{
let xm = xs[i];
buildSelect(blah,function(){ CallBack(xm,...) },...);
}

Categories

Resources