Reputation: 7952
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);
});
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.
Upvotes: 1
Views: 2264
Reputation: 708206
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);
});
});;
Upvotes: 2
Reputation: 478
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);
});
Upvotes: 2