How to maintain status of index while loading multiple images in javascript - javascript

var imageArray = [];
for(var i =0; i<3; i++){
imageArray[i] = new Image();
imageArray[i].onload = function(i) {
$("#cell_"+i).append(imageArray[i]);
imageArray[i].style.visibility = "hidden";
}
imageArray[i].onerror = function() {
alert("not loaded");
}
imageArray[i].src = '/home//dummy_'+i+'.jpg';
}
I wan to load some images from a list of dummy list of images.
but during onload i lost the context of i,
it always points to i =3 (last value of loop).
ho to preserve this i so that when it comes to onload, it will give me exact i.

You need to wrap it in another function:
imageArray[i].onload = function(ic) { // ic is a copy of i in the scope of this function
return function() {
$("#cell_"+ic).append(imageArray[ic]); // ic is borrowed from the outer scope
imageArray[ic].style.visibility = "hidden";
}
}(i); // call the function with i as parameter
Alternatively, you can use bind to ... bind a parameter to your function:
imageArray[i].onload = function(i,event) {
// added the event parameter to show that the arguments normally passed to the function are not overwritten, merely pushed to make room for the bound ones
$("#cell_"+i).append(imageArray[i]);
imageArray[i].style.visibility = "hidden";
}
}.bind(null, i); // the function will always be called with he value of i passed to the first argument
NOTE: Behind the scenes, this is the same solution.
Reference:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

Related

JQuery on click event listeners and functions, fire differently with different parameters?

I have elements that when I click on them I need to run a function and that function needs to know what element was clicked on.
Example A:
var elements = $(".config-cell");
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener("click", function() {
console.log("clicked");
});
}
When calling the function right there it works fine, however I don't know how to pass through the element to the function so it can use it.
So I tried using this method and found something strange.
Example B:
var elements = $(".config-cell");
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener("click", this.cellCrop());
}
When simply calling the function located elsewhere I noticed when loading the window it just automatically fires the function and then it doesn't even add the event listener so any time you click after that nothing happens.
Bottom line I would like to be able to pass through the current element being clicked on and have it fire a function. But I would like to know out of curiosity why method B works the way it does.
Learned that it knows which to use because 'forEach' has a callback with parameters
.forEach(function callback(currentValue[, index[, array]])
For instance: How does this call back know what is supposed to be
'eachName' and 'index'
var friends = ["Mike", "Stacy", "Andy", "Rick"];
friends.forEach(function (eachName, index){
console.log(index + 1 + ". " + eachName); // 1. Mike, 2. Stacy, 3. Andy, 4. Rick
});
And can you do this with .addEventListener instead of setting it as a
var?
That being said is there a way to have it pass variables with your own function? Like:
var passthrough = 5;
$(".config-cell").on("click", function(passthrough) {
var five = passthrough;
console.log(five);
});
First, this.cellCrop() calls the function, this.cellCrop passes it. So if you wanted to set the listener it would have been
elements[i].addEventListener("click", this.cellCrop);
Now to actually get the element clicked on inside the function you can do it a couple ways.
Using currentTarget / target from the event object that is automatically passed to event listeners
elements[i].addEventListener("click", function(event){
//the actual element clicked on
var target = event.target;
//the element the event was set on, in this case whatever elements[i] was
var currentTarget = event.currentTarget;
});
//same using jQuery
$(elements[i]).click(function(event){
var target = event.target;
var currentTarget = event.currentTarget;
});
Using the this keyword
elements[i].addEventListener("click", function(event){
//this will refer to whatever elements[i] was
var target = this;
});
//same using jQuery
$(elements[i]).click(function(event){
var target = $(this);
});
This would apply the same with using object method:
obj = {
cellCrop:function(event){
var target = event.target;
/* etc */
},
someOtherMethod:function(){
//...
elements[i].addEventListener("click",this.cellCrop);
//or jQuery
$(elements[i]).click(this.cellCrop);
//...
}
};
obj.someOtherMethod();
How does this call back know what is supposed to be 'eachName' and 'index'
Because documentation for the forEach method tells the person using it how it is going to be called. So you write the callback based on that documentation.
For instance the callback for forEach usually takes the form of
function callback(currentValue[, index[, array]])
Which means inside forEach() it is going to call your callback in this fashion
function forEach(callback){
//`this` inside forEach is the array
for(let i=0; i<this.length; i++){
callback(this[i], i, this);
}
}
As for passing arbitrary data to the function, there are a few ways it can be done:
Wrap a call to a function in an anonymous function and explicitly call the function with more arguments
obj = {
cellProp:function(event,a,b,c){
var element = event.currentTarget;
}
}
//normal anonymous function
elements[i].addEventListener('click',function(e){
obj.cellProp(e,1,2,3);
});
//fat arrow function
elements[i].addEventListener('click',e=>obj.cellProp(e,1,2,3))
In the above a, b and c will respectively contain the values 1,2 and 3
You can also use methods like bind which will change the thisArg(see this question to see more on that) of the function but also pass in arguments to the function
obj = {
//note event comes last as bind, call, and apply PREPEND arguments to the call
cellProp:function(a,b,c,event){
//`this` will change depending on the first
//argument passed to bind
var whatIsThis = this;
var element = event.target;
}
}
//passing obj as the first argument makes `this` refer to
//obj within the function, all other arguments are PREPENDED
//so a,b, and c again will contain 1,2 and 3 respectively.
elements[i].addEventListener('click', obj.cellProp.bind(obj,1,2,3) );
In the case of jQuery, you can also pass data in using an object at the time of setting up the event:
obj = {
cellProp:function(event){
var data = event.data;
console.log(data.five);
}
}
jQuery(elements[i]).click({five:5},this.cellProp);
Try this : You can make use of $(this)
$(".config-cell").on("click", function(){
var clickedCell = $(this);// this refers to current clicked cell which can be used to get other details or write logic around it
});

function receives value as undefined

I have a timeout which calls a function until 100% progress is complete. It then executes the function I have assigned to it. Only the value that was given to it is undefined or at least part of it.
I'm not sure at which stage the code is losing the value being passed, thus making it return undefined but I have made a JS Fiddle with it in action for you to see it:
JS Fiddle
My end result is to receive the value correct then remove the given element like so:
function rmv_div(div_id) {
//div_id is not properly defined so cannot find the div.
document.getElementById('result').innerHTML = div_id;
var div = document.getElementById(div_id);
div.parentNode.removeChild(div);
}
The problem is that the variable i used inside func is created outside the scope of that function, and is increased at each iteration. Then, when you call func at the end, i equals array.length, so array[i] is undefined.
You can solve it creating another variable at each iteration that you won't increase:
Solution 1:
Demo: http://jsfiddle.net/qJ42h/4/ http://jsfiddle.net/qJ42h/11/
for (var i = 0; i < array.length; i++) {
var bar = document.getElementById('bar' + array[i]),
text = document.getElementById('text' + array[i]),
remove = 'wrap' + array[i],
j = i;
do_something(bar, text, function () {
rmv_div('id' + array[j]);
}, 1);
}
Solution 2
Demo: http://jsfiddle.net/qJ42h/8/ http://jsfiddle.net/qJ42h/12/
for (var i = 0; i < array.length; i++) {
var bar = document.getElementById('bar' + array[i]),
text = document.getElementById('text' + array[i]),
remove = 'wrap' + array[i];
do_something(bar, text, (function(i) {
return function(){ rmv_div('id' + array[i]); }
})(i), 1);
}
The problem here is that you didn't isolate the loop variable i inside the closure. However, this can be solved much more elegantly by using objects.
First off, I'm introducing the object that will encapsulate what you want; it gets initialized with a bar element and a function to call when it's done counting to 100. So, I'll call it BarCounter:
function BarCounter(element, fn)
{
this.element = element;
this.fn = fn;
this.text = element.getElementsByTagName('div')[0];
this.counter = 0;
}
This is just the constructor; it doesn't do anything useful; it resolves the text element, which is simply the first <div> tag it can find underneath the given element and stores that reference for later use.
Now we need a function that will do the work; let's call it run():
BarCounter.prototype.run = function()
{
var that = this;
if (this.counter < 100) {
this.text.innerHTML = this.counter++;
setTimeout(function() {
that.run();
}, 70);
} else {
this.fn(this.element);
}
}
The function will check whether the counter has reached 100 yet; until then it will update the text element with the current value, increase the counter and then call itself again after 70 msec. You can see how the reference to this is kept beforehand to retain the context in which the run() function is called later.
When all is done, it calls the completion function, passing in the element on which the BarCounter object operates.
The completion function is much easier if you pass the element to remove:
function removeDiv(element)
{
element.parentNode.removeChild(element);
}
The final step is to adjust the rest of your code:
var array = [1];
for (var i = 0; i < array.length; ++i) {
var bar = new BarCounter(
document.getElementById('bar' + array[i]),
removeDiv
);
bar.run();
}
It's very simple now; it creates a new BarCounter object and invokes its run() method. Done :)
Btw, you have the option to remove the element from within the object as well; this, of course, depends on your own needs.
Demo

Wrong Parameter is passed to function

I have the following code that adds an onmouseover event to a bullet onload
for (var i = 0; i <= 3; i++) {
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
}
This is the function that it is calling. It is supposed to add a css class to the menu item as the mouse goes over it.
function addBarOnHover(node) {
document.getElementById('menu').getElementsByTagName('li')[node].className = "current_page_item"; }
When the function is called, I keep getting the error:
"document.getElementById("menu").getElementsByTagName("li")[node] is
undefined"
The thing that is stumping me is I added an alert(node) statement to the addBarOnHover function to see what the value of the parameter was. The alert said the value of the parameter being passed was 4. I'm not sure how this could happen with the loop I have set up.
Any help would be much appreciated.
This is a common problem when you close over an iteration variable. Wrap the for body in an extra method to capture the value of the iteration variable:
for (var i = 0; i <= 3; i++) {
(function(i){ //here
document.getElementById('menu').getElementsByTagName('li')[i].onmouseover = function () { addBarOnHover(i); };
})(i); //here
}
an anonymous function is created each time the loop is entered, and it is passed the current value of the iteration variable. i inside the anonymous function refers to the argument of this function, rather than the i in the outer scope.
You could also rename the inner variable for clarity:
for(var i=0; i<=3; i++){
(function(ii){
//use ii as i
})(i)
}
Without capturing the iteration variable, the value of i when it is finally used in the anonymous handler has been already changed to 4. There's only one i in the outer scope, shared between all instances of the handler. If you capture the value by an anonymous function, then the argument to that function is used instead.
i is being passed as a reference (not by value), so once the onmouseover callback is called, the value of i has already become 4.
You'll have to create your callback function using another function:
var menu = document.getElementById('menu');
var items = menu.getElementsByTagName('li');
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = (function(i) {
return function() {
addBarOnHover(i);
};
})(i);
}
You could make it a little more readable by making a helper function:
var createCallback = function(i) {
return function() {
addBarOnHover(i);
};
};
for (var i = 0; i <= 3; i++) {
items[i].onmouseover = createCallback(i);
}

Working with closures in JavaScript

I have this function
function createSlidingGallery(){
gallery_position = parseInt(jQuery(".imageWrapper.default").attr("rel"));
gallery_position_min = 0;
gallery_position_max = parseInt(jQuery("#galleryEl .sig_thumb a").size() - 1);
var galleryWrapper = document.createElement("div");
galleryWrapper.className = "sGalleryWrapper";
for (var i = 0; i < gallery_position_max; i++) {
var slide = document.createElement("div");
slide.className = "slide slide"+(i);
slide.setAttribute('rel', i);
galleryWrapper.appendChild(slide);
};
jQuery(".imageWrapper.top").append(galleryWrapper);
//HERE COMES THE PROBLEM PART
for (var i = 0; i < gallery_position_max; i++) {
var position = i;
//THE CALLBACK ACTUALLY USES THE SAME CONTEXT FOR ALL PARTICULAR CALLS SO THAT THE POSITION VALUE HOLDS THE MAXIMUM VALUE IN ALL INSTANCES
loadImage(position, false, function(index){
console.log(index);
jQuery(".slide"+position).css({
'background': 'url('+backgroundImages[index].src+')'
});
});
};
hideLoadingDC();
}
What it should do is asynchronously load images into dynamicaly created elements. It actually creates all the elements, and it loads images as well. But there is function called loadImage which is intended to preload images and then save the information that this images has been already loaded and are propably cached. I am calling it with callback function which handles the loaded image and sets it as a background to appropriate element.Hence I need to hold the information about the element (like pointer, or index/position).
Now I am trying to propagate its index to the function, but because the callback function is called after some time the position variable has already other value (the for loop actually goes through and in all calls of the callback it is set to the maximum value)
I know I can alter the loadImage function and add the position as another attribute, but I would prefere any other solution. I do not want to alter the loadImage function.
You can use a helper function to create a new scope for the position variable:
function makeGalleryCallback(position) {
return function(index){
console.log(index);
jQuery(".slide"+position).css({
'background': 'url('+backgroundImages[index].src+')'
});
};
}
function createSlidingGallery(){
...
for (var i = 0; i < gallery_position_max; i++) {
loadImage(i, false, makeGalleryCallback(i));
}
...
}
The problem is that all the callback functions are referencing the same i value, and not actually tracking the value at the time of iteration. So, you need to create a new scope (a closure) that creates a new reference for each different value of i.
A new scope can be created in JS with a function. Your code needs to be wrapped with an anonymous function and execute that function for each different value of i.
for (var i = 0; i < gallery_position_max; i++) {
var position = i;
loadImage(position, false, (function(index){
return function(){
console.log(index);
jQuery(".slide"+position).css({
'background': 'url('+backgroundImages[index].src+')'
});
};})(i));
};
 
 

JavaScript - Setting property on Object in Image load function, property not set once outside function

Sometimes JavaScript doesn't make sense to me, consider the following code that generates a photo mosaic based on x/y tiles. I'm trying to set a .Done property to true once each Mosaic image has been downloaded, but it's always false for some reason, what am I doing wrong?
var tileData = [];
function generate()
{
var image = new Image();
image.onload = function()
{
// Build up the 'tileData' array with tile objects from this Image
for (var i = 0; i < tileData.length; i++)
{
var tile = tileData[i];
var tileImage = new Image();
tileImage.onload = function()
{
// Do something with this tile Image
tile.Done = true;
};
tileImage.src = tile.ImageUrl;
}
};
image.src = 'Penguins.jpg';
tryDisplayMosaic();
}
function tryDisplayMosaic()
{
setTimeout(function()
{
for (var i = 0; i < tileData.length; i++)
{
var tile = tileData[i];
if (!tile.Done)
{
tryDisplayMosaic();
return;
}
}
// If we get here then all the tiles have been downloaded
alert('All images downloaded');
}, 2000);
}
Now for some reason the .Done property on the tile object is always false, even though it is explicitly being set to true inside tileImage.onload = function(). And I can ensure you that this function DOES get called because I've placed an alert() call inside it. Right now it's just stuck inside an infinite loop calling tryDisplayMosaic() constantly.
Also if I place a loop just before tryDisplayMosaic() is called, and in that loop I set .Done = true, then .Done property is true and alert('All images downloaded'); will get called.
The variable "tile" in the top loop is shared by every one of the "onload" functions you create; the very same single variable, in other words. Try this, to give each one its own variable:
tileImage.onload = (function(myTile) {
return function()
{
// Do something with this tile Image
myTile.Done = true;
};
})(tile);
Why is this so? Because unlike C or Java, declaring "tile" inside the loop like that does not make it scoped to the statement block of the loop body. The only thing that gives you scope in Javascript is a function body.
What I see is that you create closures inside loops, which is a common mistake. To avoid this, you could create a more general closure, outside the loop, which handles all the load events and executes a function after all images are loaded:
// This array must be filled somewhere...
var tileData = [];
var ImageLoader = function(){
var numOfImages = 0;
var numLoaded = 0;
var callBack = function(){};
function imageLoaded(){
numLoaded++;
if(numLoaded===numOfImages){
// All images are loaded, now call the callback function
callBack.call(this);
}
}
function init(numberOfImages, fn){
numOfImages = numberOfImages;
callBack = fn;
}
return {
imageLoaded: imageLoaded,
init: init
};
}();
function tryDisplayMosaic(){
alert('All images downloaded');
}
function generate(){
// (1) Set the number of images that are to be loaded and (all tiles + penguin)
// (2) Set the function that must be called after all images are loaded
ImageLoader.init(tileData.length+1, tryDisplayMosaic);
// Load this Penguins image
var image = new Image();
image.src = 'Penguins.jpg';
image.onload = ImageLoader.imageLoaded;
// Go through each image and load it
for (var i=0; i<tileData.length; i++) {
image = new Image();
image.src = tileData[i].ImageUrl;
image.onload = ImageLoader.imageLoaded;
}
}
This code is totally different from your code, but it's a lot prettier in my opinion. It requires that you set the number of images that you need to load and a function that must be executed after all images are loaded.
Note: I haven't tested it, I'm not sure about this .call(this) part, maybe the forum could help if it is incorrect.
I have tested my code and it works fine. Additionally I've improved it, so you save a pointer to the loaded image inside the tileData array, see JSBin for a working example.

Categories