5

I'm using the following Javascript functions to display a gallery of pictures.

function disp_pics(currObj,table){
    if(currObj != "none"){
        $("div .checkout2").removeClass("checkout2").addClass("checkout");
        $(currObj).closest("div").removeClass("checkout").addClass("checkout2");
    }

    function getData(table){
        return $.ajax({
            url: "newphoto_gallery_display.php",
            type: "GET",
            data: {
                table: table
            },
            dataType: "html"
        });
    }

    function display_result(data){
        var dfd = new $.Deferred();
        dfd.done(equalise);
        $("#ajaxoutput").html(data);
        setTimeout(function() {
            dfd.resolve();
        }, 1000);
    }

    function equalise(){
        var highestBox = 0;
        $('.photodisplay').each(function(){
            if($(this).height() > highestBox)  {
                highestBox = $(this).height(); 
            }
        });  
        $('.photodisplay').height(highestBox);
    }

    var promise=getData(table);
    promise.success(function (data) {
        promise.done(display_result(data));
    });
};

The function getData fetches the picture data from a database. The function display_result then outputs that data to the div id "ajaxoutput". Pictures are displayed along with relevant data in boxes (HTML tables with a border). The function equalise is then called to make all the boxes of equal height.

Without the time delay in the display_result the equalise function is called before all the pictures and data have been displayed and therefore messes up the display. Is there a way to do away with the time delay and only call equalise function when display_result has finished outputting all the data to the ajaxoutput div?

I've tried all sorts of Deferred's and $.when.......then.... combinations, but haven't managed to achieve the desired result without delaying the script, which isn't ideal. Any suggestions would be welcome.

2
  • What is the promise.done supposed to accomplish? It's already done via the success. Commented Dec 16, 2014 at 19:42
  • You could use either callbacks or promises Commented Dec 16, 2014 at 20:49

3 Answers 3

5

If the issue is that you need to know when all the images the HTML that you applied to #ajaxoutput have been loaded (and thus their size is known to the browser), then you can do that like this by monitoring the onload event for each of the images you added to the HTML:

function display_result(data){
    var dfd = new $.Deferred();
    var imgWaiting = 0;
    $("#ajaxoutput").html(data).find("img").each(function() {
        if (!this.complete) {
            // img is still loading, install a load handler so we know when it's done
            ++imgWaiting;
            this.onload = this.onerror = function() {
               --imgWaiting;
               if (imgWaiting === 0) {
                   dfd.resolve();
               }
            }
        }
    });
    if (imgWaiting === 0) {
         dfd.resolve();
    }
    return dfd.promise();
}

Then, you can do:

return getData(table).then(display_result).then(equalise);

Which is a classic design pattern with promises for serializing several async operations.

Sign up to request clarification or add additional context in comments.

4 Comments

I need to check this on a slower computer, but on my PC this works! Thank you so much! Been struggling with this for days!
If HTMLImageElement.complete is reliable (under whatever !Doctype is being used) then this answer should be reliable. However, experience has taught me not to trust HTMLImageElement.complete.
@Roamer-1888 - I have always found img.complete to be reliable when an image is being originally loaded for the first time. I have not always found .complete or the load event to be reliable when setting a new .src property on the image (particularly in older versions of IE if my memory serves me). But, the OP's example is only when loading the image for the first time, so I think .complete is fine here.
IIRC ... HTMLImageElement.complete was only formalised in HTML5. Under earlier Doctypes support was (and still is) patchy, and some early HTML5 implementations (esp FF) were buggy. It's probably one of those things that's fixed in the current crop of browsers but people like me will stick to old tried and tested techniques for a while yet.
2

You need to get rid of the timeout delay as it's not guaranteed to be long enough for the images to have loaded (slow server, slow internet etc).

Unfortunately the "proper" solution you have been seeking is not the simplest, due mainly to the fact that you can only access the img nodes of interest after the statement $("#ajaxoutput").html(data) puts them in place. In the statement following that one, it will be uncertain (due to browser behaviour) whether the images are already loaded (from cache) or not. Simply attaching an "onload" handler would therefore be unreliable.

A workaround is to create a bunch of off-screen img nodes that mirror those in "#ajaxoutput" and to "promisify" them in such a way that their 'src' properties are set after attaching "onload" (and "onerror") handlers.

The code is not simple to follow so I've done my best to provide explanatory comments.

function display_result(data) {
    //First a function that creates a promisified <img> node, and returns its promise.
    //<img> nodes created here are not appended to the DOM.
    function imgPromise(index, imgNode) {
        return $.Deferred(function(dfrd) {
            //Note the order here - the onload/onerror handler is attached before the src attribute is set.
            //Note also that both onload/onerror cause the deferred to be resolved, as you don't want any failures to prevent calling `equalise()`.
            $("<img/>").on('load error', dfrd.resolve).attr('src', imgNode.src);
        }).promise();
    }

    //A lot happens in the next line ...
    // * "#ajaxoutput" is given some HTML
    // * any freshly created image nodes in "#ajaxoutput" are discovered
    // * `imgPromise()` is called for each img node in turn and the returned promises are stuffed into an array
    // * the array is assigned to local var `promises`.
    var promises = $("#ajaxoutput").html(data).find("img").map(imgPromise);

    //Finally, `$.when()` returns a promise that is resolved when all `promises` have settled.
    //This "summary promise" is returned.
    return $.when.apply(null, promises);
}

Without the comments, you will see that this is actually very concise and hopefully a little less scary.

function display_result(data) {
    function imgPromise(index, imgNode) {
        return $.Deferred(function(dfrd) {
            $("<img/>").on('load error', dfrd.resolve).attr('src', imgNode.src);
        }).promise();
    }
    var promises = $("#ajaxoutput").html(data).find("img").map(imgPromise);
    return $.when.apply(null, promises);
}

Call as follows :

getData(table).then(display_result).then(equalise);

5 Comments

Thanks for the well explained code, but the function equalise doesn't get called when I try this.
How far does it get? What does eg console.log(promises.length) give?
Whoops, I got the signature of jQuery's .map() callback wrong. One line edited above.
Why create duplicate image objects when you can just check the state of the images already in the DOM?
Because I don't trust HTMLImageElement.complete, on which that would rely.
-1

Can you run them synchronously?

function display_result(data){
    $("#ajaxoutput").html(data);
    equalise();
}

I'm pretty sure the .html function is a synchronous call, so your resizing will happen after this.

Edited: What does the data look like coming from your ajax call? Is it just a bunch of img tags? If so, you'd probably be better off loading the image in ajax and encoding it into the html yourself.

3 Comments

The whole point of the question is that the OP can't run them synchronously. That doesn't work.
No, have tried that, but equalise is called before all the HTML is displayed in #ajaxoutput. The data looks like this:
Sorry - edit timed out. No, have tried that, but equalise is called before all the HTML is displayed in #ajaxoutput. The data is too long for me to post here, but consists of a series of tables, displayed inline, each of which contains an image and information (such as title, size etc.). The number of table rows containing the information. differs from table to table, therefore making the tables different heights. Therefore I need to make all the table heights equal after they display in order to tidy up the display.

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.