How to use Deferred objects in a for loop - javascript

I'm not sure about how to ask that.
I want to know the size of three images from which I have only the URL (needless to say, that's just an example).
Something tells me that Deferred is the way to do it... I've used it in other situations but I don't know how to chain various deferred objects together, one for each iteration of the for loop, and to have one done function for all of them (does it makes sense? I don't know if I'm making myself clear).
That's basically what I'm trying to do with this code, but naturally here the deferred will call done only once, and not for each image. I would like Deferred to be resolved only after I have the dimensions of all images.
update
I understand now, thanks to jgreenberg and Bergi, that I need a Deferred object for each image, as well as an array of Promises. New code:
var imagesURLArray = [
'http://placekitten.com/200/300',
'http://placekitten.com/100/100',
'http://placekitten.com/400/200'];
var promises = [];
function getImageDimensions(url){
var deferred = new $.Deferred();
var img = $('<img src="'+ imagesURLArray[i] +'"/>').load(function(){
deferred.resolve( {width: this.width, height: this.height} );
});
return deferred.promise();
}
for (var i = 0; i < imagesURLArray.length; i++) {
promises.push(getImageDimensions(imagesURLArray[i]));
}
$.when.apply($, promises).then(function(dimensions){
console.log('dimensions: ' + dimensions);
});
Sample here
However I still can't figure out how to retrieve data from all Deferred objects in then(). The dimensions argument returns only data from the first Deferred object.

Here's a little piece of code that extends the built-in jQuery promises to work for the .load() events for images. This allows you to do some very simple coding to wait for your group of images to load. Here's the add-on code for jQuery to support promises for image loading:
(function() {
// hook up a dummy animation that completes when the image finishes loading
// If you call this before doing an animation, then animations will
// wait for the image to load before starting
// This does nothing for elements in the collection that are not images
// It can be called multiple times with no additional side effects
var waitingKey = "_waitingForLoad";
var doneEvents = "load._waitForLoad error._waitForLoad abort._waitForLoad";
jQuery.fn.waitForLoad = function() {
return this.each(function() {
var self = $(this);
if (this.tagName && this.tagName.toUpperCase() === "IMG" &&
!this.complete && !self.data(waitingKey)) {
self.queue(function(next) {
// nothing to do here as this is just a sentinel that
// triggers the start of the fx queue
// it will get called immediately and will put the queue "inprogress"
}).on(doneEvents, function() {
// remove flag that we're waiting,
// remove event handlers
// and finish the pseudo animation
self.removeData(waitingKey).off(doneEvents).dequeue();
}).data(waitingKey, true);
}
});
};
// replace existing promise function with one that
// starts a pseudo-animation for any image that is in the process of loading
// so the .promise() method will work for loading images
var oldPromise = jQuery.fn.promise;
jQuery.fn.promise = function() {
this.waitForLoad();
return oldPromise.apply(this, arguments);
};
})();
This works by overriding the current .promise() method and when the .promise() method is called for each image in the collection that has not yet completed loading, it starts a dummy animation in the jQuery "fx" queue and that dummy animation completes when the image's "load" event fires. Because jQuery already supports promises for animations, after starting the dummy animation for each image that has not yet loaded, it can then just call the original .promise() method and jQuery does all the work of creating the promise and keeping track of when the animation is done and resolving the promise when the animations are all done. I'm actually surprised jQuery doesn't do this themselves because it's such a small amount of additional code and leverages a lot of things they're already doing.
Here's a test jsFiddle for this extension: http://jsfiddle.net/jfriend00/bAD56/
One very nice thing about the built-in .promise() method in jQuery is if you call it on a collection of objects, it will return to you a master promise that is only resolved when all the individual promises have been resolved so you don't have to do all that housekeeping - it will do that dirty work for you.
It is a matter of opinion whether the override of .promise() is a good way to go or not. To me, it seemed nice to just add some additional functionality to the existing .promise() method so that in addition to animations, it also lets you manage image load events. But, if that design choice is not your cup of tea and you'd rather leave the existing .promise() method as it is, then the image load promise behavior could be put on a new method .loadPromise() instead. To use it that way, instead of the block of code that starts by assigning oldPromise = ..., you would substitute this:
jQuery.fn.loadPromise = function() {
return this.waitForLoad().promise();
};
And, to retrieve a promise event that includes the image load events, you would just call obj.loadPromise() instead of obj.promise(). The rest of the text and examples in this post assume you're using the .promise() override. If you switch to .loadPromise(), you will have to substitute that in place in the remaining text/demos.
The concept in this extension could also be used for images in the DOM too as you could do something like this so you could do something as soon as a set of images in the DOM was loaded (without having to wait for all images to be loaded):
$(document).ready(function() {
$(".thumbnail").promise().done(function() {
// all .thumbnail images are now loaded
});
});
Or, unrelated to the original question, but also something useful you can do with this extension is you can wait for each individual images to load and then kick off an animation on each one as soon as it's loaded:
$(document).ready(function() {
$(".thumbnail").waitForLoad().fadeIn(2000);
});
Each image will fade in starting the moment it is done loading (or immediately if already loaded).
And, here's how your code would look using the above extension:
var imagesURLArray = [
'http://placekitten.com/200/300',
'http://placekitten.com/100/100',
'http://placekitten.com/400/200'];
// build a jQuery collection that has all your images in it
var images = $();
$.each(imagesURLArray, function(index, value) {
images = images.add($("<img>").attr("src", value));
});
images.promise().done(function() {
// all images are done loading now
images.each(function() {
console.log(this.height + ", " + this.width);
});
});
Working jsFiddle Demo: http://jsfiddle.net/jfriend00/9Pd32/
Or, in modern browsers (that support .reduce() on arrays), you could use this:
imagesURLArray.reduce(function(collection, item) {
return collection.add($("<img>").attr("src", item));
}, $()).promise().done(function() {
// all images are done loading now
images.each(function() {
console.log(this.height + ", " + this.width);
});
});;

You need to use http://api.jquery.com/jquery.when/
I tried to modify your code but I haven't run it. I believe you'll figure it out from here.
function getImageDimensions(url){
var deferred = new $.Deferred();
var img = $('<img src="'+ imagesURLArray[i] +'"/>').load(function(){
var dimensions = [this.width, this.height];
deferred.resolve(dimensions);
});
return deferred.promise()
}
var promises = []
for (var i = 0; i < imagesURLArray.length; i++) {
promises.push(getImageDimensions(imagesURLArray[i]));
}
$.when.apply($, promises).then(function(dimensions){
console.log('dimensions: ' + dimensions);
});

Related

jQuery run function after everything has run

I want to add colspan with jQuery to an element. If I insert this function into console in Chrome then it works fine, but if I put it in document ready and run it in the script then nothing happens. This function should run after all scripts have run if possible. Any tips will help, no access to HTML.
$( document ).ready(function() {
$("#fieldset_Q8aGrid td:eq(4)").attr('colSpan',3)
});
You probably have some asynchronous scripts running which add the content after your code runs. If you do not have access to that other code, then the most pragmatic solution is to try from time to time until it succeeds:
$( document ).ready(function() {
var timer = setInterval(function () {
if ($("#fieldset_Q8aGrid td:eq(4)").attr('colSpan',3).length) {
clearInterval(timer);
}
}, 500); // check every half-second.
});
I am assuming you have some async code in your program that, for example, create dynamic HTML. This is the case if you don't know when all the functions will completely execute.
We can split this problem into two parts.
If you call a function X after a sync code Y, X will happen after Y.
But if you have call a function X after an async code Y, you don't know what will execute first.
To solve 1) you just have to put your code at the end of your script.
To solve 2) you have to use jQuery's deferred and when to explicitly tell your program: execute this when X, Y, Z is done.
Quick explanation (and quite not precise) of what a promise is:
A promise is an object with a then method (thenable). The then method
can receive a function that will be called when the promise is
resolved
From an animation you can use the .promise() method to create a promise that will be resolved when the animation is done.
var animationPromise = $('h1')
.css({'left': '0', 'position' : 'relative'})
.animate({'left': '20px'}, 700, 'linear')
.promise()
If you are making a call to a service, the jQuery's post, get and ajax, return a promise
// save the promise for later use
var asyncPromise = $.get('https://jsonplaceholder.typicode.com/users');
// execute some the function that uses this data
asyncPromise.then(function(users) { /* do something with the data */ })
Maybe, you have some functionality that is neither an AJAX or an animation, in this case you can create your own promise. For example, this promise is resolved from a setTimeout.
var backgroundChange = $.Deferred()
setTimeout(function() {
$('body').css('background', 'lightblue')
backgroundChange.resolve(true)
}, 3000) /* Please note that backgroundChange
doesn't know that it will be resolved after 3 seconds */
Later on, you can use .when(...) to create a promise that is resolved when a set of promises are resolved.
$.when(asyncPromise, backgroundChange, animationPromise)
.then(function() {
$('.container').css('color', 'blue')
//alert('Everything is done!');
})
Here is a working example that solves the two problems https://codepen.io/anon/pen/JWBXLd?editors=1010
You can either rearrange your scripts to change their order, or go with window.load:
$(window).load(function() {
// code
});
But this will fire after literally all your page is loaded. Even images. So it'll be better to go with the first approach.

javascript : calculating and returning values after Image loaded

I already checked multiple responses to asynchronous javascript behaviours and experimented with callbacks, experimenting with promises next, but parallell I want to send SoS request here :/
Because often first answers are ("What do you want to do?")
I have to find out all the non-empty or transparent pixels in an Image.(kinda like a mask) and give them back an array of binary arithmetical numbers.
What my program does in short:
it gets Image URL and creates new Image() from it.
it creates a Canvas and adds it to DOM
in image.onLoad() it draws the image on the canvas
after the Image is on canvas, it scans every pixel for its color and gives back a data array.
My problem is to force the calculation to WAIT until the image is loaded. I tried to do with something like this:
return getDataArray(image.onLoad()= function(){
// ...init things...
}));
Still, it goes into the getDataArray function, before the image.onLoad happens.
I'm gonna take a break and walk outside because I'm out of productive ideas.
Here is the original function:
getImageScan: function() {
this.myImage.src = imageScanner.imgUrl;
var is = this;
return is.getArrayFromCanvas(this.myImage.onload = function () {
is.imageHeight = is.myImage.height;
is.imageWidth = is.myImage.width;
is.appendCanvasToBody();
is.initGraphicalContent();
is.drawImageOnCanvas();
console.log("image is loaded");
})
},
getArrayFromCanvas: function () {
console.log("request for calculations");
var booleanJson = this.getJsonFromCanvas()
return this.getBinaryArithmeticFromBooleans(booleanJson);
}
and this is the result
request for calculations
[]
the image is loaded
here is the entire *.js if you want more information (it's my private project in slacktime, so no copyright issues):
https://github.com/Vilkaz/gridToImage/blob/master/web/resources/js/imageScanner.js
You try to pass something to getArrayFromCanvas although it has no parameters. I don't understand why you do this, but I guess you want something like this:
getImageScan: function(callback) {
this.myImage.src = imageScanner.imgUrl;
var is = this;
this.myImage.onload = function () {
is.imageHeight = is.myImage.height;
is.imageWidth = is.myImage.width;
is.appendCanvasToBody();
is.initGraphicalContent();
is.drawImageOnCanvas();
console.log("image is loaded");
callback(is.getArrayFromCanvas());
}
}
One difference between the asynchronous behavior above and your original code is that getImageScan returns immediately and that a callback is called later to "return" the result.

Issue with waiting for an event to be done then execute another

I'm trying to use jQuery Promises to solve a problem.
I have a UI in which I make an API call and waiting for a response (Which takes a while). I want to wait for the response to see if everything is loaded then do an action. How can I achieve this?
This is my code:
var d1 = new $.Deferred();
if(($('.fa-spin').length + $('.chart-loading').length )==0){
d1.resolve();
} //Logic behind here is, I'm checking for loading icon,
loading spinner and once they are destroyed,
I want to execute something!
$.when(d1).then(function() {
console.log("fetched");
//Run something
});
I want to check if my loading spinner is done, and then fire an event. How can I do this?
When I execute this code, the deferred never gets resolved and fetch is never printed
you'll need to continually check in order to resolve:
var d1 = new $.Deferred();
var testInterval = setInterval(function() {
if(($('.fa-spin').length + $('.chart-loading').length )==0){
clearInterval(testInterval);
d1.resolve();
}
},100);
$.when(d1).then(function() {
console.log("fetched");
//Run something
});
this will check every .1sec if the spinner's gone, and resolve your defer object when the condition is met. although, you could just run your code there instead of as a defer/resolve.
additionally it might be more effective to hook into your actual process rather than the existence of a spinner dom object, consider implementing an existing observer pattern or roll your own: observer pattern

jQuery: Detecting if browser is done rendering after append()

In a jQuery project, I am adding several images to a page after it has finished loading, using the append() function. After all the appends are done, I need to call a function that detects the width and height of each of these new elements, so I can properly align them in the page.
This is working well on my rather slow development PC. But when testing it on a fast machine in a quick browser (e.g. Chrome), it turns out that calling the layout function right after the appends are done leads to random, weird behavior. Executing the layout function again with a slight delay solves the problem. I guess "javascript is done appending the new elements" is not the same as "the browser is done displaying them", so measuring their size is not yet possible.
So what I need is some way to tell when the new elements are actually there, so that I can reliably detect their widths and heights. I don't think document.ready() would help in this case, since the elements are added by a function that is called in document.load().
This is the code I copied from another stackoverflow thread (by a dude named Pointy, thanks by the way!)
function called_by_document_load() {
var promises = [];
for (var i=0; i<my_pics.length; i++) {
(function(url, promise) {
var img = new Image();
img.onload = function () {
promise.resolve();
};
img.src = url;
}) (my_pics[i]['url'], promises[i] = $.Deferred());
}
$.when.apply($, promises).done(function() {
// add the new images using append()
// when done, call my own layout function
}
... with the array my_pics containing the URLs of the images to load.
Any ideas? :-)
Thanks in advance,
snorri
After adding the new images using append, yield the control to the browser so that the browser can update the ui and then invoke your layout function. You can do so my using a setTimeout with delay set to 0.
$.when.apply($, promises).done(function() {
// add the new images using append()
setTimeout(function () {
// when done, call my own layout function
}, 0);
}
There are many SO threads that explain why this works and is useful.
Here is one good explanation
Also, worth watching is the video by Philip Roberts on browser's event loop.

clearInterval() and setInterval() when all animations are complete

I have a function that is repeatedly being called with setInterval creating animations. If there are still animations running I need it to stop calling the function until all the animations are complete. The code I am using is as follows:
EDIT: Added coded where I am removing the animated elements from the DOM, is that the problem?
var serviceLoop = setInterval(serviceQueue, LOOP_POLL_MS); //10 ms
function serviceQueue()
{
//do some animations..
function moveMan(from, to, plane)
{
(function() {
var tmp_from = from;
var tmp_to = to;
var tmp_plane = plane;
var pos = tmp_from.offset();
var temp = tmp_from.clone(true);
temp.css({ "visibility":"visible",
"position":"absolute",
"top":pos.top + "px",
"left":pos.left + "px"});
temp.appendTo("body");
tmp_from.css("visibility", "hidden");
//if (!tmp_plane) tmp_to.css("visibility", "hidden");
temp.animate(to.offset(), moveMan.ANIMATION_TIME, function() {
tmp_to.css("visibility", "visible");
temp.remove();
});
})();
}
if ($(":animated").length > 0)
{
clearInterval(serviceLoop);
$(":animated").promise().done(function() {
serviceLoop = setInterval(serviceQueue, LOOP_POLL_MS);
});
}
}
The problem I am having is after a couple of animations the function passed to done() is never called, and the script stops.
It seems likely that you end up waiting on a promise() that is waiting on some animations, but then you remove some of those objects from the DOM and then their animation never finishes so the promise never resolves.
See this quote from the jQuery doc for .promise():
Note: The returned Promise is linked to a Deferred object stored on
the .data() for an element. Since the.remove() method removes the
element's data as well as the element itself, it will prevent any of
the element's unresolved Promises from resolving. If it is necessary
to remove an element from the DOM before its Promise is resolved, use
.detach() instead and follow with .removeData() after resolution.
One quick hack might be top call .stop(true) on any item that you are removing from the DOM.
In general, this code needs to be rewritten to be safe from that possibility and hopefully to rethink how you approach whatever it is you're trying to do every 10ms because that's generally a bad design. You should use events to trigger changes, not a 10ms polling operation. You have not explained the purpose of this code so it's not clear to me what alternative to suggest.

Categories