Unable to register event handler to array of elements in javascript - javascript

function recordData(){
var element = document.getElementsByClassName("bubble");
for(var i = 0; i < element.length; i++){
element[i].addEventListener("click", function(){
var id = element[i].attributes.id.value;
var x_cordinate = element[i].children[2].attributes.x.value;
var y_cordinate = element[i].children[2].attributes.y.value;
var keyword = element[i].children[0].textContent;
clicked_elements.push({
id: id,
x_cordinate: x_cordinate,
y_cordinate: y_cordinate,
keyword: keyword
})
}, false);
}
}
When i try to click the elements in html it shows an error "Uncaught TypeError: Cannot read property 'attributes' of undefined". I cannot access the id and other attributes of that particular element in addEventListener method. Please explain what i am doing wrong here if i need to record what is being clicked.

You can't use i inside the addEventListener callback because it will be executed after the end of the for loop.
Use this instead of elements[i] to get the current element inside the closure.

Your problem here is that the variable i won't have the value you think it'd have. That's due to how closures work.
Try this simple example:
for(var i = 0; i < 5; i++) {
console.log("Before timeout: " + i);
setTimeout(function() {
console.log("After timeout: " + i);
}, 1000);
}
What would you expect when you run this? Probably that after one second, you see this in the console?
Before timeout: 0
Before timeout: 1
Before timeout: 2
Before timeout: 3
Before timeout: 4
After timeout: 0
After timeout: 1
After timeout: 2
After timeout: 3
After timeout: 4
But guess what, you'll see this:
Before timeout: 0
Before timeout: 1
Before timeout: 2
Before timeout: 3
Before timeout: 4
After timeout: 5
After timeout: 5
After timeout: 5
After timeout: 5
After timeout: 5
Why 5? The for statement basically sets i to 0, then counts it up every iteration and checks every time whether it is still below 5, and if not, breaks the loop. So, after the loop, i has value 5, like this:
for(var i = 0; i < 5; i++) {
/* ... */
}
console.log("After loop: " + i);
...gives:
After loop: 5
Okay but why is the value used which i has after the loop? That's because your function callback you are passing to setTimeout accesses the i variable in the outside scope directly! And because the function will only run after one second (after the loop was long completed), the value of i at this time will be 5, as shown above!
So the solution would be to create a local copy of this variable which is local to each iteration. Now this is not as trivial as it seems because in JavaScript, control blocks like for don't create a scope (unless you use ES6 and let). You have to use another anonymous function around it to create a local copy which is then different each time:
for(var i = 0; i < 5; i++) {
// This is a so-called "immediately executed function expression"
// It's an anonymous function which is then immediately called due
// to the `()` at the end.
(function() {
var j = i; // Now we have a local `j`
console.log("Before timeout: " + j);
setTimeout(function() {
console.log("After timeout: " + j);
}, 1000);
})();
}
Another way to due this, which is a bit shorter, is this trick:
for(var i = 0; i < 5; i++) {
// See how this anonymous function now takes a parameter `i`?
// And we pass this parameter when calling this function below,
// which basically creates an inner copy which we can now also call
// `i`.
(function(i) {
console.log("Before timeout: " + i);
setTimeout(function() {
console.log("After timeout: " + i);
}, 1000);
})(i);
}
This is why I recommend to use things like forEach when iterating over arrays because they take a function callback as parameter so you automatically have a local scope in your code in there.
Now in your case, you could either use one of the anonymous-function-solutions from above, or something like forEach, but there is a gotcha: The element variable is no normal JavaScript array, it's a list of DOM elements, which doesn't have forEach. But you can still make it work using the Array.prototype.forEach.call trick:
function recordData(){
var element = document.getElementsByClassName("bubble");
Array.prototype.forEach.call(element, function(i) {
element[i].addEventListener("click", function(){
var id = element[i].attributes.id.value;
var x_cordinate = element[i].children[2].attributes.x.value;
var y_cordinate = element[i].children[2].attributes.y.value;
var keyword = element[i].children[0].textContent;
clicked_elements.push({
id: id,
x_cordinate: x_cordinate,
y_cordinate: y_cordinate,
keyword: keyword
})
}, false);
});
}
Before, it didn't work because you were effectively using element[element.length].attributes (because i was already one beyond your upper bound of the array), so element[i] is was undefined.
There you go!
However, an even better way would be using this because inside an event listener, this refers to the target of the event. So instead of element[i], you can just use this everywhere inside your listener!

You are running on a common issue with Javascript, in your 'for' you are registering the event handlers based on i value, when the event it's fired the value of 'i' is equals to 'element.length' that returns undefined this is the reason that you get your error.
To solve this you need to make a clousere in order to keep a reference to the element of the array not it's index. Try to use Array.forEach instead of a 'for'.
Here yoy con fin more info of how closures work.

function recordData(){
var element = document.getElementsByClassName("bubble");
for(var i = 0; i < element.length; i++){
element[i].addEventListener("click", function(){
var id = this.attributes.id.value;
var x_cordinate = this.children[2].attributes.x.value;
var y_cordinate = this.children[2].attributes.y.value;
var keyword = this.children[0].textContent;
clicked_elements.push({
id: id,
x_cordinate: x_cordinate,
y_cordinate: y_cordinate,
keyword: keyword
})
}, false);
}
}
This is the final version of my code which runs fine. 'this' helped me access the object inside the event listener.

Related

How can I make a for loop in Javascript that will set timeouts from an array?

Background (You might want to skip this)
I'm working on a web app that animates the articulation of English phonemes, while playing the sound. It's based on the Interactive Sagittal Section by Daniel Currie Hall, and a first attempt can be found here.
For the next version, I want each phoneme to have it's own animation timings, which are defined in an array, which in turn, is included in an object variable.
For the sake of simplicity for this post, I have moved the timing array variable from the object into the function.
Problem
I set up a for loop that I thought would reference the index i and array t to set the milliseconds for each setTimeout.
function animateSam() {
var t = [0, 1000, 2000, 3000, 4000];
var key = "key_0";
for (var i = 0; i < t.length; i++) {
setTimeout(function() {
console.log(i);
key = "key_" + i.toString();
console.log(key);
//do stuff here
}, t[i]);
}
}
animateSam()
However, it seems the milliseconds are set by whatever i happens to be when the function gets to the top of the stack.
Question: Is there a reliable way to set the milliseconds from the array?
The for ends before the setTimeout function has finished, so you have to set the timeout inside a closure:
function animateSam(phoneme) {
var t = [0,1000,2000,3000,4000];
for (var i = 0; i < t.length; i++) {
(function(index) {
setTimeout(function() {
alert (index);
key = "key_" + index.toString();
alert (key);
//do stuff here
}, t[index]);
})(i);
}
}
Here you have the explanation of why is this happening:
https://hackernoon.com/how-to-use-javascript-closures-with-confidence-85cd1f841a6b
The for loop will loop all elements before the first setTimeout is triggered because of its asynchronous nature. By the time your loop runs, i will be equal to 5. Therefore, you get the same output five times.
You could use a method from the Array class, for example .forEach:
This ensures that the function is enclosed.
[0, 1000, 2000, 3000, 4000].forEach((t, i) => {
setTimeout(function() {
console.log(i);
console.log(`key_${i}`);
//do stuff here
}, t)
});
Side note: I would advise you not to use alert while working/debugging as it is honestly quite confusing and annoying to work with. Best is to use a simple console.log.
Some more clarifications on the code:
.forEach takes in as primary argument the callback function to run on each of element. This callback can itself take two arguments (in our previous code t was the current element's value and i the current element's index in the array):
Array.forEach(function(value, index) {
});
But you can use the arrow function syntax, instead of defining the callback with function(e,i) { ... } you define it with: (e,i) => { ... }. That's all! Then the code will look like:
Array.forEach((value,index) => {
});
This syntax is a shorter way of defining your callback. There are some differences though.
I would suggest using a function closure as follows:
function animateSam(phoneme) {
var t = [0,1000,2000,3000,4000];
var handleAnimation = function (idx) {
return function() {
alert(idx);
key = "key_" + idx.toString();
alert(key);
//do stuff here
};
}
for (var i = 0; i < t.length; i++) {
setTimeout(handleAnimation(i), t[i]);
}
}
I this example you wrap the actual function in a wrapper function which captures the variable and passes on the value.

JavaScript closure loop issue in set time out

I've found some example in a tutorial (said it was the canonical example)
for (var i=1; i<=5 ; i++) {
setTimeout(function() {
console.log("i: " + i);
}, i*1000);
}
Now, I understand that, closure passes in the current scope in to the function, and I assume that it should output 1,2,3,4,5. But instead, it prints number 6 five times.
I ran it in the chrome debugger, and first it goes through the loop without going in to the function while doing the increment of the i value and only after that, it goes in to the inner function and execute it 5 times.
I'm not sure why its doing that, I know, the current scoped is passed in to the function because of closure, but why does it not execute each time the loop iterate?
By the time the timeout runs, the for loop has finished, and i is 6, that's why you're getting the output you see. You need to capture i during the loop:
for (var i=1; i<=5 ; i++) {
(function(innerI) {
setTimeout(function() {
console.log("i: " + innerI);
}, innerI*1000);
})(i);
}
This creates an inner function with it's own parameter (innerI), that gets invoked immediately and so captures the value of i for use within the timeout.
If you didn't want the complex-looking IIFE as explained in James' answer, you can also separate out the function using bind:
function count(i) {
console.log("i: " + i);
}
for (var i = 1; i <= 5; i++) {
setTimeout(count.bind(this, i), i * 1000);
}
Thank you for you help,
I found out another solution and it was a minor change.
On the top of the page I turned on the strict mode and also in the for loop, Instead of var, I used the "let" keyword.

$.get in a loop: why the function is performed after incrementing?

I am a beginner in Javascript and I feel that there is something wrong with me about the $.get jQuery.
Normally, you can assign it to a function that will execute after the data is retrieved correctly.
But if I put my $.get in a loop, the loop continues to execute even if the data is not yet retrieved, and here is my problem.
Here is my code (this is for GreaseMonkey):
var1 = document.getElementsByClassName("some_class");
i = 0;
while (i < var1.length) {
url = var1[i].getElementsByTagName("some_tag")[0].href;
$.get(url, function(data) {
if (data.contains("some_string")) {
alert(i);
}
});
i++;
}
Here, the alert returns var1.length event if it should returns 1 for exemple.
I try to put an alert(i) just after the url declaration and I understood that i++ was done before the function in my $.get.
This is surely a trivial problem, but I can not grasp the logic to not make this happen.
Wrap your $.get function thus:
(function(i) {
$.get(url, function(data) {
if (data.contains("some_string")) {
alert(i);
}
});
})(i);
The immediately invoked function expression causes the current value of i that's in the outer scope to be bound via the function's parameter i (which then hides the outer variable). If you like, give the function parameter a different name.
Note that this only fixes the problem you actually stated, which is that the loop variable is incremented independently of the callbacks. If you wish to ensure that the AJAX requests run one at a time then there are other solutions, e.g.:
var els = document.getElementsByClassName("some_class");
var i = 0;
(function loop() {
if (i < els.length) {
var el = els[i];
var url = el.getElementsByTagName("some_tag")[0].href;
$.get(url).done(function(data) {
if (data.contains("some_string")) {
alert(i);
}
i++;
}, loop); // .done(f1, f2) - see below
}
})();
The .done() call is in the form .done(callback, loop) and the two functions will be called in order. So the i++ line always happens first, and then it arranges for loop to be called pseudo-recursively to process the next element.
Since you're using jQuery, you can simplify your code quite a bit:
$('.some_class').each( function( i, element ) {
var url = $(element).find('some_tag')[0].href;
$.get( url, function( data ) {
if( data.contains("some_string") ) {
alert( i );
}
});
});
Changes from the original code are:
jQuery calls instead of the getElementsBy* functions.
jQuery .each() for the loop.
Added missing var where needed. (Very important in any version of the code!)
Note that the use of .each() automatically gives you the same effect as the immediately invoked function expression (IIFE) in another answer, but without the extra complication. That's because .each() always uses a callback function, and that creates the closure needed to preserve the i variable (and element too) uniquely for each iteration of the loop.
You can also do this when you have an ordinary while or for loop, and you still don't need the IIFE. Instead, simply call a function in the loop. Written this way, the code would be:
var $elements = $('.some_class');
for( var i = 0; i < $elements.length; i++ ) {
checkElement( i, $elements[i] );
}
function checkElement( i, element ) {
var url = $(element).find('some_tag')[0].href;
$.get( url, function( data ) {
if( data.contains("some_string") ) {
alert( i );
}
});
}
As you can see, the checkElement function is identical to the .each() callback function. In fact, .each() simply runs a similar for loop for you and calls the callback in exactly the same way as this code. Also, the for loop is more readable than the while loop because it puts all the loop variable manipulation in one place. (If you're not familiar with the for loop syntax it may seem less readable at first, but once you get used to it you will probably find that you prefer the for loop.)
In general, when tempted to use an IIFE in the middle of a loop, try breaking that code out into a completely separate function instead. In many cases it leads to more readable code.
Here's a little demo for you to investigate further.
$("#output").empty();
var startTime = new Date().getTime();
// try experimenting with async = true/false and the delay
// don't set async to false with too big a delay,
// and too high a count,
// or you could hang your browser for a while!
// When async==false, you will see each callback respond in order, followed by "Loop finished".
// When async==true, you could see anything, in any order.
var async = true;
var delay = 1;
var count = 5;
function createClosure(i) {
// return a function that can 'see' i.
// and i's remains pinned within this closure
return function (resp) {
var duration = new Date().getTime() - startTime;
$("#output").append("\n" + i + " returned: " + resp + " after " + duration + "ms");
};
}
for (var i = 0; i < count; i++) {
// jsfiddle url and params
var url = "/echo/html/";
var data = {
html: "hello " + i,
delay: delay
};
$.ajax(url, {
type: "post",
data: data,
async: async
}).then(createClosure(i));
}
var duration = new Date().getTime() - startTime;
$("#output").append("\n" + "Loop finished after " + duration + "ms");
Sample async=true output:
Loop finished after 7ms
0 returned: hello 0 after 1114ms
1 returned: hello 1 after 1196ms
2 returned: hello 2 after 1199ms
4 returned: hello 4 after 1223ms
3 returned: hello 3 after 1225ms
Sample async=false output (and the browser hangs for 5558ms!):
0 returned: hello 0 after 1113ms
1 returned: hello 1 after 2224ms
2 returned: hello 2 after 3329ms
3 returned: hello 3 after 4444ms
4 returned: hello 4 after 5558ms
Loop finished after 5558ms

Passing parameters into a closure for setTimeout

I've run into an issue where my app lives in an iframe and it's being called from an external domain. IE9 won't fire the load event when the iframe loads properly so I think I'm stuck using setTimeout to poll the page.
Anyway, I want to see what duration is generally needed for my setTimeout to complete, so I wanted to be able to log the delay the setTimeout fires from my callback, but I'm not sure how to pass that context into it so I can log it.
App.readyIE9 = function() {
var timings = [1,250,500,750,1000,1500,2000,3000];
for(var i = 0; i < timings.length; i++) {
var func = function() {
if(App.ready_loaded) return;
console.log(timings[i]);
App.readyCallBack();
};
setTimeout(func,timings[i]);
}
};
I keep getting LOG: undefined in IE9's console.
What's the proper method to accomplish this?
Thanks
This is happening because you are not closing around the value of i in your func. When the loop is done, i is 8 (timings.length), which doesn't exist in the array.
You need to do something like this:
App.readyIE9 = function() {
var timings = [1,250,500,750,1000,1500,2000,3000];
for(var i = 0; i < timings.length; i++) {
var func = function(x) {
return function(){
if(App.ready_loaded) return;
console.log(timings[x]);
App.readyCallBack();
};
};
setTimeout(func(i),timings[i]);
}
};
When your function gets called by setTimeout sometime in the future, the value of i has already been incremented to the end of it's range by the for loop so console.log(timings[i]); reports undefined.
To use i in that function, you need to capture it in a function closure. There are several ways to do that. I would suggest using a self-executing function to capture the value of i like this:
App.readyIE9 = function() {
var timings = [1,250,500,750,1000,1500,2000,3000];
for(var i = 0; i < timings.length; i++) {
(function(index) {
setTimeout(function() {
if(App.ready_loaded) return;
console.log(timings[index]);
App.readyCallBack();
}, timings[index]);
})(i);
}
};
As a bit of explanation for who this works: i is passed to the self-executing function as the first argument to that function. That first argument is named index and gets frozen with each invocation of the self-executing function so the for loop won't cause it to change before the setTimeout callback is executed. So, referencing index inside of the self-executing function will get the correct value of the array index for each setTimeout callback.
This is a usual problem when you work with setTimeout or setInterval callbacks. You should pass the i value to the function:
var timings = [1, 250, 500, 750, 1000, 1500, 2000, 3000],
func = function(i) {
return function() {
console.log(timings[i]);
};
};
for (var i = 0, len = timings.length; i < len; i++) {
setTimeout(func(i), timings[i]);
}
DEMO: http://jsfiddle.net/r56wu8es/

How to pass variable to anonymous function

I want to pass variable setTimeoutfunction and do something with that. When I alert value of i it shows me numbers that i did not expected. What i m doing wrong? I want log values from 1 till 8.
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(function (i) {
console.log(i);
}, 800);
}
The standard way to solve this is to use a factory function:
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(makeResponder(i), 800);
}
function makeResponder(index) {
return function () {
console.log(index);
};
}
Live example | source
There, we call makeResponder in the loop, and it returns a function that closes over the argument passed into it (index) rather than the i variable. (Which is important. If you just removed the i argument from your anonymous function, your code would partially work, but all of the functions would see the value of i as of when they ran, not when they were initially scheduled; in your example, they'd all see 8.)
Update From your comments below:
...will it be correct if i call it in that way setTimeout(makeResponder(i),i*800);?
Yes, if your goal is to have each call occur roughly 800ms later than the last one, that will work:
Live example | source
I tried setTimeout(makeResponder(i),setInterval(i));function setInterval(index) { console.log(index*800); return index*800; } but it's not work properly
You don't use setInterval that way, and probably don't want to use it for this at all.
Further update: You've said below:
I need first iteration print 8 delay 8 sec, second iteration print 7 delay 7 sec ........print 2 delay 2 sec ...print 0 delay 0 sec.
You just apply the principles above again, using a second timeout:
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(makeResponder(i), i * 800);
}
function makeResponder(index) {
return function () {
var thisStart = new Date();
console.log("index = " + index + ", first function triggered");
setTimeout(function() {
console.log("index = " +
index +
", second function triggered after a further " +
(new Date() - thisStart) +
"ms delay");
}, index * 1000);
};
}
Live example | source
I think you now have all the tools you need to take this forward.
Your problem is that you are referring to the variable i some time later when your setTimeout() function fires and by then, the value of i has changed (it's gone to the end of the for loop. To keep each setTimeout with it's appropriate value of i, you have to capture that value i separately for each setTimeout() callback.
The previous answer using a factory function does that just fine, but I find self executing functions a little easier than factory functions to type and follow, but both can work because both capture the variables you want in a closure so you can reference their static value in the setTimeout callback.
Here's how a self executing function would work to solve this problem:
var end=8;
for (var i = 1; i < end; i ++) {
(function (index) {
setTimeout(function() {
console.log(index);
}, 800);
})(i);
}
To set the timeout delay in proportion to the value of i, you would do this:
var end=8;
for (var i = 1; i < end; i ++) {
(function (index) {
setTimeout(function() {
console.log(index);
}, index * 800);
})(i);
}
The self executing function is passed the value of i and the argument inside that function that contains that value is named index so you can refer to index to use the appropriate value.
Using let in ES6
With the ES6 of Javascript (released in 2015), you can use let in your for loop and it will create a new, separate variable for each iteration of the for loop. This is a more "modern" way to solve a problem like this:
const end = 8;
for (let i = 1; i < end; i++) { // use "let" in this line
setTimeout(function() {
console.log(i);
}, 800);
}
The main reason for this to not to work, is because, of the setTimeout which is set to run after 800 and the scope of i.
By the time it executes which the value of i will already have changed. Thus no definitive result could be received. Just like TJ said, the way to work this around is through a handler function.
function handler( var1) {
return function() {
console.log(var1);
}
}
var end = 8;
for (var i = 1; i < end; i++) {
setTimeout(handler(i), 800);
}
Demo
setTimeout accepts variables as additional arguments:
setTimeout(function(a, b, c) {
console.log(a, b, c);
}, 1000, 'a', 'b', 'c');
Source.
EDIT: In your example, the effective value of i will likely be 8, since the function is merely to be called after the loop has finished. You need to pass the current value of i for each call:
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(function (i) {
console.log(i);
}, 800, i);
}

Categories

Resources