JavaScript closure for jQuery event handler - javascript

I have a list of objects each of which has a .bullet which is a SPAN. I want to bind a click on the span to a handler than performs a certain action on the span using jQuery. I'm seeing some behavior I don't understand, so I'm hoping someone can explain what's going on. Basically, this first code example works:
for (var i = 0 ; i< length ; i++) {
(function(){
dataNode = dataNodeList[i];
var handler = function(e) {
e.data.node.bullet.firstChild.nodeValue = "- ";
};
$(dataNode.bullet).on("click",{node:dataNode},handler);
})();
}
However, this second variation does not work:
for (var i = 0 ; i< length ; i++) {
(function(){
dataNode = dataNodeList[i];
var handler = function() {
dataNode.bullet.firstChild.nodeValue = "- ";
};
$(dataNode.bullet).on("click",handler);
})();
}
In this second example,
dataNode.bullet.firstChild.nodeValue = "- ";
has no effect on the value of the SPAN I intended. I expected dataNode.bullet to still point to the SPAN I want to change because of JavaScript closure. So, can someone explain why this fails? Thanks.

Try this:
for (var i = 0 ; i< length ; i++) {
(function(index){
var dataNode = dataNodeList[index];
var handler = function() {
dataNode.bullet.firstChild.nodeValue = "- ";
};
$(dataNode.bullet).on("click",handler);
})(i);
}
The closure defines a new scope. This is necessary because your handler isn't called until after the loop has finished, so i is not part of the scope at the time it is called, or (in some cases) has the last possible value from the loop.

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)
}

Creating a closure for .setTimeout() inside a for loop

I am trying to write a javascript program which stores the value from an input element in an array when a button is clicked. The array is the split and each individual letter added to a span element and then appended to the document. The idea is to create a typing effect using setTimeout.
I am running into an issue creating a closure within the loop, so currently the setTimeout function always returns the final value of the iteration.
The function in question is at the bottom of the code block and called addTextToBoard();
var noteButton = document.querySelector('[data-js="button"]');
noteButton.addEventListener("click",function() {
var messageIn = document.querySelector('[data-js="input"]');
var message = messageIn.value;
postToBoard(message);
});
function postToBoard(val) {
var noteBoard = document.querySelector('[data-js="noteboard"]');
var newElement = document.createElement('div');
newElement.classList.add('noteboard__item');
noteBoard.appendChild(newElement);
setTimeout(function(){
newElement.classList.add('active');
}, 200);
addTextToBoard(newElement, val);
}
function addTextToBoard(el, val) {
var wordArray = val.split('');
for(i = 0; i < wordArray.length; i++) {
var letter = document.createElement('span');
letter.innerHTML = wordArray[i];
setTimeout(function(x){
return function() {}
el.appendChild(letter);
}(i),1000);
}
}
I believe I am close, I'm just not fully understanding the syntax for creating the closure. If someone could give poke in the right direction, without necessarily giving the full solution that would be great.
I essentially tried to paste in the following code snippet from here but I've missed something somehwere along the way!
setTimeout(function(x) { return function() { console.log(x); }; }(i), 1000*i);
Best,
Jack
You are close.
Since the "letter" variable changes, you'll add only the last letter over and over again. You need to "save" the current letter on the setTimeout() callback function, One way to go is like this:
function appendMyLetter(letter) {
return(function() {
el.append.Child(letter);
});
}
function addTextToBoard(el, val) {
var wordArray = val.split('');
for(i = 0; i < wordArray.length; i++) {
var letter = document.createElement('span');
letter.innerHTML = wordArray[i];
setTimeout(appendMyLetter(letter), 1000);
}
}
This way, the appendMyLetter() function gets called with a different parameter (one for each letter) and returns a function with the correct "stored" value to be called by setTimeout().
EDIT
Looking at your setTimeout() code closely
setTimeout(function(x){
return function() {}
el.appendChild(letter);
}(i),1000);
It would work fine, if you used the proper parameters and used the appendChild() inside the returned function, like so:
setTimeout(function(x){
return(function() {
el.appendChild(x);
});
}(letter),1000);
You can create an immediately-invoked function expression IIFE to create a closure
function addTextToBoard(el, val) {
var wordArray = val.split('');
for(i = 0; i < wordArray.length; i++) {
(function(index) {
var letter = document.createElement('span');
letter.innerHTML = wordArray[i];
setTimeout(function(){
el.appendChild(letter);
},1000);
})(i);
}
}
I dont know if this will work but here you go a slight change in operator:
letter.innerHTML += wordArray[i];
if you dont get the effect you imagined you will get you better try to increment the timer by i like this
setTimeout(function(){
...
},1000*i);

Simple count function does not work

I thought making a simple function where if you click on a button a number will show up inside of a paragraph. And if you continue to click on the button the number inside the paragraph tag will increase. However, I'm getting an error message saying that getElementsByTagName is not a function. Here is the code on jsfiddle, I know there is something simple that I'm doing wrong but I don't know what it is.
HTML
<div class="resist" id="ex1"><h2>Sleep</h2><p></p><button>Resist</button></div>
<div class="resist" id="ex2"><h2>Eat</h2><p></p><button>Resist</button></div>
Javascript
var count = 0;
var resist = document.getElementsByClassName('resist') ;
for(var i = 0; i < resist.length; i++)
{ var a = resist[i];
a.querySelector('button').addEventListener('click', function(a){
count +=1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
}
You are overwriting a variable with event object passed into event handler. Change the name to e maybe, or remove it altogether as you are not using it anyway:
a.querySelector('button').addEventListener('click', function(e /* <--- this guy */) {
count += 1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
Another problem you are going to have is classical closure-in-loop issue. One of the solutions would be to use Array.prototype.forEach instead of for loop:
var count = 0;
var resist = Array.prototype.slice.call(document.getElementsByClassName('resist'));
// ES6: var resist = Array.from(document.getElementsByClassName('resist'));
resist.forEach(function(a) {
a.querySelector('button').addEventListener('click', function(e) {
count += 1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
});
vars in Javascript are function scoped, so you must wrap your event listener binding in a closure function to ensure the variable you're trying to update is correctly set.
(Note: I've renamed a to div in the outer function and removed the arg from the inner click function).
var count = 0;
var resist = document.getElementsByClassName('resist') ;
var div;
for(var i = 0; i < resist.length; i++)
{
div = resist[i];
(function(div){
div.querySelector('button').addEventListener('click', function(){
count +=1;
div.getElementsByTagName('p')[0].innerHTML = count;
});
})(div);
}

Get the specific div id from an onclick event (Pure JS no Jquery)

When I try the code referenced in SO #1, I get the console logging a blank string:
installChoices() {
var choices = this.game.page.options;
for (var i = 0; i < choices.length; i++) {
var choice = choices[i];
var choiceDiv = document.createElement("choice" + i);
choiceDiv.innerText = choice[0];
choiceDiv.onclick = function() {
console.log(this.id);
}
this.choicesContainer.appendChild(choiceDiv);
}
}
I want to bind to my class function clicked
installChoices() {
var choices = this.game.page.options;
for (var i = 0; i < choices.length; i++) {
var choice = choices[i];
var choiceDiv = document.createElement("choice" + i);
choiceDiv.innerText = choice[0];
choiceDiv.onclick = this.clicked;
this.choicesContainer.appendChild(choiceDiv);
}
}
clicked(e) {
console.log(e.parentNode); //this is undefined
console.log(e.srcElement);
}
But that shows undefined. When I log srcElement, I get the full element
<choice0>path 1</choice0>
I want to get just the div id when I click, so I can parse that and do logic.
I'd recommend the following approach, as it is the standard:
//assign the event
choiceDiv.addEventListener('click', clicked)
//define the listener
function clicked(event) {
console.log(event.currentTarget)
}
update:
I'm tempted to offer a fix to your code, because I don't think you're achieving what are you trying to actually do:
function installChoices() {
var choices = this.game.page.options;
for (var i = 0; i < choices.length; i++) {
var choice = choices[i];
var choiceDiv = document.createElement("div");
choiceDiv.id = "choice" + i;
choiceDiv.innerText = choice[0];
choiceDiv.addEventListener("click", clicked);
this.choicesContainer.appendChild(choiceDiv);
}
}
function clicked(ev) {
console.log(ev.currentTarget.id); //this will log "choice0"
}
Your "clicked" function are receiving an Event rather than a HTML Element. This is the default behavior when an onClick event triggered.
The click event have a srcElement property, indicating the source element the click event occurred upon. This event object have no parentNode property.
Use e.srcElement.parentNode instead.
BTW, in the SO #1 example, it assign "showIt(this)" to onClick, so browser pass "this", the target element rather than the event object, to the onClick function.

java script last iteration set the value for all iterations

var myElements = document.getElementsByName('bb1');
for (var i = 0; i < myElements.length; i++) {
var curValue = myElements[i].getAttribute('innerId')
myElements[i].addEventListener('mouseover', function () {
alert('Hello i am : ' + curValue);
}, false);
}
when mouse over, every element, instead of showing a different value for curValue, a constant value (the last iteration value) is displayed.
what am i doing wrong here?
There is no different scope inside blocks like for in JavaScript, so when your mouseover event is triggered, it will alert the current variable value which was set in the last iteration.
You can just use this inside your callback function to get the attribute of the object which the event was triggered.
var myElements = document.getElementsByName('bb1');
for (var i = 0; i < myElements.length; i++) {
myElements[i].addEventListener('mouseover', function () {
alert('Hello i am : ' + this.getAttribute('innerId'));
}, false);
}
The general issue here is the closure in Javascript. This happens when using variable (in this case curValue) not defined within the callback function.
I recommend reading answers about JS closures here.

Categories

Resources