1

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.

4
  • 1
    Look at jQuery .when(). Commented Jul 15, 2014 at 19:32
  • 1
    jQuery load is not reliable for images cross browser Commented Jul 15, 2014 at 19:46
  • 1
    You can resolve a deferred only once, so you will need a different deferred for each image. You can combine them with $.when if that's what you want. Commented Jul 15, 2014 at 21:09
  • @update: See this thread on how to access the results of the merged deferreds. (Or use a proper Promise library) Commented Jul 16, 2014 at 1:38

2 Answers 2

2

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);
    });
});;
Sign up to request clarification or add additional context in comments.

11 Comments

+1 for the effort, even if I oppose overwriting $.fn.promise
@Bergi - The way I've written it, one could remove the replacement of .promise() and require the manual calling of .waitForLoad() before calling .promise() as in .waitForLoad().promise(), but I was going for a more built-in behavior by just building it into .promise() and it's a no-op for any objects in the collection that are not images in the process of loading. Just curious, why do you oppose the replacement of $.fn.promise? It just makes one method call and then calls and returns the original method verbatim.
It just doesn't fit in there imho. I'd rather have .waitForLoad() returning a promise by appending the .promise() to the .each(…) call inside of it :-)
@Bergi - .promise() works for animations in jQuery and any other queued actions. Why can't it also work for image loading? It's jQuery description is this: Return a Promise object to observe when all actions of a certain type bound to the collection, queued or not, have finished. This seems to fit right into that to me. It also has a really nice feature in that when you call it on a collection, it returns a promise that resolves when all the individual items of the collection are resolved which saves manual $.when() coding and I wanted to take advantage of that too.
@Bergi - I didn't want to return a promise from .waitForLoad() because it is useful by itself to start an animation as soon as an image is loaded by chaining it like this $(imageSelector).waitForLoad().animate(...) without even involving promises.
|
2

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

4 Comments

Thanks, It seems to be very close though I still get only the dimensions of the first file. I managed to accomplish with a not so elegant solution here
I would convert the image array to an image map{url:width} instead of passing the value back in the deferred . I've never passed data back in a deferred like this.
@jgreenebrg: In fact, you should always pass the data back into the deferred.resolve. Even if jQuery fails at passing usable results from $.when
@Bergi Makes sense, I've never had a reason to do it. It just makes sense to me that you wouldn't have access in the result of the when.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.