Variable in wrong scope (maybe needs a closure?) - javascript

I have the following code that is in need of a closure:
var numItems = document.getElementsByClassName('l').length;
for (var i = 0; i < numItems; i++) {
document.getElementsByClassName('l')[i].onclick = function (e){
preview(this.href, i);
};
}
What happens is that whenever an item is clicked, preview always the same number for i
I suspect what I need to do is
function indexClosure(i) {
return function(e) {
preview(this.href, i);
}
}
And assign the onclick's like this:
document.getElementsByClassName('l')[i].onclick = indexClosure(i);
But then this would no longer refer to my link... how is this problem solved?

Use closure to capture the counter of the cycle:
var numItems = document.getElementsByClassName('l').length;
for (var i = 0; i < numItems; i++) {
(function(i){
document.getElementsByClassName('l')[i].onclick = function (e){
preview(this.href, i);
};
}(i))
}

doesn't onclick pass in (sender, eventArgs) allowing you to access this through sender?

Related

Event listeners and closures on HTML collection in for loop

The code is
//Logic which works when the desired element is clicked
function changeArtistPhotoAndBio(prop) {
var artistPhoto = document.getElementsByClassName("artist-photo")[0];
var artistBio = document.getElementsByClassName("artist-bio")[0];
var i = prop.getAttribute("src").indexOf(".jpg");
var photoName = prop.getAttribute("src").slice (0, i);
artistPhoto.style.background="url(" + photoName + "-large.jpg";
console.log("it happened");
};
//Setting listeners for the click event in the loop
var artists = document.getElementsByClassName("gallery")[0].getElementsByTagName("img");
for (var i = 0; i < artists.length; i++) {
artists[i].addEventListener("click", changeArtistPhotoAndBio(artists[i]));
}
And the console output is
7x it happened
And the event handler for the click function does not work. I've tried isolating handler in the closure, like this:
for (var i = 0; i < artists.length; i++) {(function(i) {
artists[i].addEventListener("click", changeArtistPhotoAndBio(artists[i]));
}(i))
}
but the output is still the same. So there are two questions:
1) Why does the console output contain results of seven handler invocations if I did not invoke the function, just set it as a handler?
2) How can I set handlers in the "for" loop for HTML collection?
You have to use closures:
var artists = document.getElementsByClassName("gallery")[0].getElementsByTagName("img");
for (var i = 0; i < artists.length; i++) {
artists[i].addEventListener("click", function(index) {
return function() {
//You can use index for the current clicked item index
// console.log(index);
var artistPhoto = document.getElementsByClassName("artist-photo")[0];
var artistBio = document.getElementsByClassName("artist-bio")[0];
var i = this.getAttribute("src").indexOf(".jpg");
var photoName = this.getAttribute("src").slice (0, i);
artistPhoto.style.background="url(" + photoName + "-large.jpg";
console.log("it happened");
}
}(i));
}
$('body *').on('mouseover',function(){console.log(this.tagName)});
$('body *') selects all elements within the body.

Javascript Pass parameter to function inside variable

I'm trying to assign a click handler to a JQuery object, defined in a variable :
some.object.array[8].action = function(data){console.log(data);}
anotherobject = {..}
now inside some loop, I need to assign this function to the click handler:
and want to pass the whole 'anotherobject' object
for (var i = 0; i < foo.length; i++) {
$('<div/>').click(some.object.array[i].action);
}
But how can I pass the parameter?
If I encapsulate it inside some anonymous function, I'm losing my scope...:
for (var i = 0; i < foo.length; i++) {
$('<div/>').click(function() {
some.object.array[i].action(anotherobject)
});
}
because i has changed...
How are we supposed to do this?
There are just too many ways to do this:
for (var i = 0; i < foo.length; i++) {
(function(i) {
$('<div/>').click(function() {
some.object.array[i].action(anotherobject);
});
})(i);
}
Or
for (var i = 0; i < foo.length; i++) {
$('<div/>').data("i", i).click(function() {
var i = $(this).data("i");
some.object.array[i].action(anotherobject);
});
});
}
Or
function getClickHandler(callback, parameter) {
return function() { callback(parameter); };
};
for (var i = 0; i < foo.length; i++) {
$('<div/>').click(getClickHandler(some.object.array[i].action, anotherobject));
}
If you want your action function to maintain the div as this and still accept the jQuery event object, you can use bind like this example:
function action(another, event){
console.log(this, arguments);
}
$(function(){
for (var i = 0; i<10; i++) {
var anotherObject = "another"+i;
var div = $('<div>'+i+'</div>');
// force 'this' to be 'div.get(0)'
// and 'arg0' to be 'anotherObject'
div.click(action.bind(div.get(0),anotherObject));
$("body").append(div);
}
})
Example: https://jsfiddle.net/Lwe5b9cx/ You'll need to open the console to see the output, which should look like this:
for(var i = 0; i < foo.length; i++)
{
$('<div/>').click(
(
return function(callback){
callback(anotherobject)
}
)(some.object.array[i].action)
);
}

Setting onclick function to <li> element

I am trying to dynamically add onclick function to "li" tagged elements.
But the event does not fires.
Here is my code:
var arrSideNavButtons = [];
var sideNavLi = document.getElementsByClassName('side-nav')[0].getElementsByTagName('li');
var arrayOfSceneAudios = [scene1Audio, scene2Audio,...];
for (var i = 0; i < sideNavLi.length; i++) {
sideNavLi[i].onclick = function() {
arrayOfSceneAudios[i].play();
}
arrSideNavButtons.push(sideNavLi[i]);
}
Is it possible to code it this way?
If yes, what is my mistake?
Thanks a lot.
Wrap your onclick handler in a closure, else it only get assigned to the last elem in the loop:
for (var i = 0; i < sideNavLi.length; i++) {
(function(i) {
sideNavLi[i].onclick = function() {
arrayOfSceneAudios[i].play();
}
arrSideNavButtons.push(sideNavLi[i]);
})(i)
}
I think it's better to reuse one single function, instead of creating a new one at each iteration:
var arrSideNavButtons = [],
sideNavLi = document.getElementsByClassName('side-nav')[0].getElementsByTagName('li'),
arrayOfSceneAudios = [scene1Audio, scene2Audio,...],
handler = function() {
this.sceneAudio.play();
};
for (var i = 0; i < sideNavLi.length; i++) {
sideNavLi[i].sceneAudio = arrayOfSceneAudios[i];
sideNavLi[i].onclick = handler;
arrSideNavButtons.push(sideNavLi[i]);
}

Javascript for loop and alert

I am looping through a list of links. I can correctly get the title attribute, and want it displayed onclick. When the page is loaded and when I click on a link, all of the link titles are alerted one by one. What am I doing wrong?
function prepareShowElement () {
var nav = document.getElementById('nav');
var links = nav.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
links[i].onclick = alert(links[i].title);
}
}
What you were doing was actually running the alert function.
enclosing the whole thing in an anonymous function will only run it when it is clicked
for (var i = 0; i < links.length; i++) {
links[i].onclick = function () {
alert(this.title);
}
}
You are assigning the onclick to the return value of alert(links[i].title); which doesn't make any sense, since onclick is supposed to be a function.
What you want instead is somethig like onclick = function(){ alert('Hi'); };
But
Since you are using a variable i in that loop you need to create a local copy of it
onclick = function(){ alert(links[i].title); }; would just use the outer scope i and all your links would alert the same message.
To fix this you need to write a function that localizes i and returns a new function specific to each link's own onclick:
onclick = (function(i){ return function(e){ alert(links[i].title); }; })(i);
Final result:
function prepareShowElement () {
var nav = document.getElementById('nav');
var links = nav.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
links[i].onclick = (function(i){ return function(e){ alert(links[i].title); }; })(i);
}
}
You can use jquery. To display title of the link on click.
$("#nav a").click(function() {
var title = $(this).attr('title');
alert(title);
});
links.forEach(function(link) {
link.onclick = function(event) {
alert(link.title);
};
}
Also note that your original solution suffered from this problem:
JavaScript closure inside loops – simple practical example
By passing in our iteration variable into a closure, we get to keep it. If we wrote the above using a for-loop, it would look like this:
// machinery needed to get the same effect as above
for (var i = 0; i < links.length; i++) {
(function(link){
link.onclick = function(event) {
alert(link.title);
}
})(links[i])
}
or
// machinery needed to get the same effect as above (version 2)
for (var i = 0; i < links.length; i++) {
(function(i){
links[i].onclick = function(event) {
alert(links[i].title);
}
})(i)
}
You need change .onclick for a eventlistener same:
function prepareShowElement () {
var nav = document.getElementById('nav');
var links = nav.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click',function() {
alert(links[i].title);
},false);
}
}

How does JavaScript closure work in this case?

How does JavaScript closure work in this case and to be more specific: what does the (i) at the end do?
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
Also I'm trying to implement it in my code, and it seems I don't get it right
for (var i=0; i < len; i++) {
var formID = document.forms["form-" + i];
$(formID).bind("submit", validate);
$(formID).bind("change", function(i){
var divI = '#ind-' + i;
$(divI).css("background-color","green");
})(i);
}
This is a pattern used to create local scope around a variable. If this wasn't used then every call to console.log(i) would log the value of i (10) after the for loop finished.
for(var i = 0; i < 10; i++) {
// create new function
(function(e) {
// log each counter after 1 second.
setTimeout(function() {
console.log(e);
}, 1000);
// execute it with the counter
})(i);
}
The above is the same as this.
function foobar(e) {
setTimeout(function() {
console.log(e);
}, 1000);
}
for (var i = 0; i < 10; i++) {
(
foobar
)(i);
}
The real problem here is creating functions in a loop. don't do it :)
Your code
for (var i=0; i < len; i++) {
var formID = document.forms["form-" + i];
$(formID).bind("submit", validate);
// create a full closure around the block of code
(function() {
$(formID).bind("change", function(i){
var divI = '#ind-' + i;
$(divI).css("background-color","green");
})//(i); Don't call (i) here because your just trying to execute the
// jQuery element as a function. You can't do this, you need to wrap
// an entire function around it.
})(i);
}
But that is wrong, you want to delegate this job to something else.
function makeGreen(form, i) {
$(form).change(function() {
$("#ind-"+i).css("background-color", "green");
});
}
for (var i=0; i < len; i++) {
var formID = document.forms["form-" + i];
$(formID).bind("submit", validate);
// call a helper function which binds the change handler to the correct i
makeGreen(formID, i);
}
If you want to get a bit clever you can get rid of these anonymous functions
function makeGreen() {
var divId = $(this).data("div-id");
$(divId).css("background-color", "green");
}
for (var i=0; i < len; i++) {
$(document.forms["form-" + i])
.bind("submit", validate)
// store i on the form element
.data("div-id", "#ind-" + i)
// use a single event handler that gets the divId out of the form.
.change(makeGreen);
}
Edit
( // contain the function we create.
function(parameterA) {
window.alert(parameterA);
}
) // this now points to a function
("alertMessage"); // call it as a function.
Is the same as
( // contain the window.alert function
window.alert
) // it now points to a function
("alertMessage"); // call it as a function
Although not a direct answer to the closure question, here is my take on the issue.
I would re-write the logic to avoid the need for a closure (as it seems overcomplicated for the requirements)
The fact that there is a pattern in the naming of the forms makes things really easy
$('form[id^="form-"]').submit(validate)
.change(function(){
var divI = '#ind-' + this.id.replace('form-','');
$(divI).css("background-color","green");
});
demo http://jsfiddle.net/gaby/q8WxV/

Categories

Resources