How to detect when an iframe has already been loaded

It seems that $('#someIframe').load(function(){...}) won't fire if it is attached after the iframe has finished loading. Is that correct?

What I'd really like is to have a function that is always called once when or after an iframe has loaded. To make this clearer, here are two cases:

  • Iframe hasn't loaded yet: run a callback function once it loads.
  • Iframe has already loaded: run the callback immediately.

How can I do this?

Answers:

Answer

I've banged my head against a wall until I found out what's happening here.

Background information

  • Using .load() isn't possible if the iframe has already been loaded (event will never fire)
  • Using .ready() on an iframe element isn't supported (reference) and will call the callback immediately even if the iframe isn't loaded yet
  • Using postMessage or a calling a container function on load inside the iframe is only possible when having control over it
  • Using $(window).load() on the container would also wait for other assets to load, like images and other iframes. This is not a solution if you want to wait only for a specific iframe
  • Checking readyState in Chrome for an alredy fired onload event is meaningless, as Chrome initializes every iframe with an "about:blank" empty page. The readyState of this page may be complete, but it's not the readyState of the page you expect (src attribute).

Solution

The following is necessary:

  1. If the iframe is not loaded yet we can observe the .load() event
  2. If the iframe has been loaded already we need to check the readyState
  3. If the readyState is complete, we can normally assume that the iframe has already been loaded. However, because of the above-named behavior of Chrome we furthermore need to check if it's the readyState of an empty page
  4. If so, we need to observe the readyState in an interval to check if the actual document (related to the src attribute) is complete

I've solved this with the following function. It has been (transpiled to ES5) successfully tested in

  • Chrome 49
  • Safari 5
  • Firefox 45
  • IE 8, 9, 10, 11
  • Edge 24
  • iOS 8.0 ("Safari Mobile")
  • Android 4.0 ("Browser")

Function taken from jquery.mark

/**
 * Will wait for an iframe to be ready
 * for DOM manipulation. Just listening for
 * the load event will only work if the iframe
 * is not already loaded. If so, it is necessary
 * to observe the readyState. The issue here is
 * that Chrome will initialize iframes with
 * "about:blank" and set its readyState to complete.
 * So it is furthermore necessary to check if it's
 * the readyState of the target document property.
 * Errors that may occur when trying to access the iframe
 * (Same-Origin-Policy) will be catched and the error
 * function will be called.
 * @param {jquery} $i - The jQuery iframe element
 * @param {function} successFn - The callback on success. Will 
 * receive the jQuery contents of the iframe as a parameter
 * @param {function} errorFn - The callback on error
 */
var onIframeReady = function($i, successFn, errorFn) {
    try {
        const iCon = $i.first()[0].contentWindow,
            bl = "about:blank",
            compl = "complete";
        const callCallback = () => {
            try {
                const $con = $i.contents();
                if($con.length === 0) { // https://git.io/vV8yU
                    throw new Error("iframe inaccessible");
                }
                successFn($con);
            } catch(e) { // accessing contents failed
                errorFn();
            }
        };
        const observeOnload = () => {
            $i.on("load.jqueryMark", () => {
                try {
                    const src = $i.attr("src").trim(),
                        href = iCon.location.href;
                    if(href !== bl || src === bl || src === "") {
                        $i.off("load.jqueryMark");
                        callCallback();
                    }
                } catch(e) {
                    errorFn();
                }
            });
        };
        if(iCon.document.readyState === compl) {
            const src = $i.attr("src").trim(),
                href = iCon.location.href;
            if(href === bl && src !== bl && src !== "") {
                observeOnload();
            } else {
                callCallback();
            }
        } else {
            observeOnload();
        }
    } catch(e) { // accessing contentWindow failed
        errorFn();
    }
};

Working example

Consisting of two files (index.html and iframe.html): index.html:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Parent</title>
</head>
<body>
    <script src="https://code.jquery.com/jquery-1.12.2.min.js"></script>
    <script>
        $(function() {

            /**
             * Will wait for an iframe to be ready
             * for DOM manipulation. Just listening for
             * the load event will only work if the iframe
             * is not already loaded. If so, it is necessary
             * to observe the readyState. The issue here is
             * that Chrome will initialize iframes with
             * "about:blank" and set its readyState to complete.
             * So it is furthermore necessary to check if it's
             * the readyState of the target document property.
             * Errors that may occur when trying to access the iframe
             * (Same-Origin-Policy) will be catched and the error
             * function will be called.
             * @param {jquery} $i - The jQuery iframe element
             * @param {function} successFn - The callback on success. Will 
             * receive the jQuery contents of the iframe as a parameter
             * @param {function} errorFn - The callback on error
             */
            var onIframeReady = function($i, successFn, errorFn) {
                try {
                    const iCon = $i.first()[0].contentWindow,
                        bl = "about:blank",
                        compl = "complete";
                    const callCallback = () => {
                        try {
                            const $con = $i.contents();
                            if($con.length === 0) { // https://git.io/vV8yU
                                throw new Error("iframe inaccessible");
                            }
                            successFn($con);
                        } catch(e) { // accessing contents failed
                            errorFn();
                        }
                    };
                    const observeOnload = () => {
                        $i.on("load.jqueryMark", () => {
                            try {
                                const src = $i.attr("src").trim(),
                                    href = iCon.location.href;
                                if(href !== bl || src === bl || src === "") {
                                    $i.off("load.jqueryMark");
                                    callCallback();
                                }
                            } catch(e) {
                                errorFn();
                            }
                        });
                    };
                    if(iCon.document.readyState === compl) {
                        const src = $i.attr("src").trim(),
                            href = iCon.location.href;
                        if(href === bl && src !== bl && src !== "") {
                            observeOnload();
                        } else {
                            callCallback();
                        }
                    } else {
                        observeOnload();
                    }
                } catch(e) { // accessing contentWindow failed
                    errorFn();
                }
            };

            var $iframe = $("iframe");
            onIframeReady($iframe, function($contents) {
                console.log("Ready to got");
                console.log($contents.find("*"));
            }, function() {
                console.log("Can not access iframe");
            });
        });
    </script>
    <iframe src="iframe.html"></iframe>
</body>
</html>

iframe.html:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Child</title>
</head>
<body>
    <p>Lorem ipsum</p>
</body>
</html>

You can also change the src attribute inside index.html to e.g. "http://example.com/". Just play around with it.

Answer

I'd use postMessage. The iframe can assign its own onload event and post to the parent. If there are timing issues just make sure to assign the parent's postMessage handler before creating the iframe.

For this to work the iframe must know the url of the parent, for instance by passing a GET parameter to the iframe.

Answer

I tried very hard to come to a solution that worked consistently cross browser. IMPORTANT: I was not able to come to such a solution. But here is as far as I got:

// runs a function after an iframe node's content has loaded
// note, this almost certainly won't work for frames loaded from a different domain
// secondary note - this doesn't seem to work for chrome : (
// another note - doesn't seem to work for nodes created dynamically for some reason
function onReady(iframeNode, f) {
    var windowDocument = iframeNode[0].contentWindow.document;
    var iframeDocument = windowDocument?windowDocument : iframeNode[0].contentWindow.document;

    if(iframeDocument.readyState === 'complete') {
        f();
    } else {
        iframeNode.load(function() {
            var i = setInterval(function() {
                if(iframeDocument.readyState === 'complete') {
                    f();
                    clearInterval(i);
                }
            }, 10);
        });
    }
}

and I was using it like this:

onReady($("#theIframe"), function() {
    try {
        var context = modal[0].contentWindow;
        var i = setInterval(function() {
            if(context.Utils !== undefined && context.$) { // this mess is to attempt to get it to work in firefox
                context.$(function() {
                    var modalHeight = context.someInnerJavascript();

                    clearInterval(i);
                });
            }
        }, 10);
    } catch(e) { // ignore
        console.log(e);
    }
});

Note that even this does not solve the problem for me. Here are some problems with this solution:

  • In onReady, for iframes that were added dynamically, iframeDocument.readyState seems to be stuck at "uninitialized" and thus the callback never fires
  • The whole setup still doesn't seem to work in firefox for some reason. It almost seems like the setInterval function is cleared externally.
  • Note that some of these problems only happen when there is a lot of other stuff loading on the page, which makes the timing of these things less deterministic.

So if anyone can improve upon this, it would be much appreciated.

Answer

Only when the content inside the iframe is loaded innerDoc is true and fires code inside the if.

    window.onload = function(){
 function manipulateIframe(iframeId, callback) {
     var iframe = document.getElementById(iframeId).contentWindow.document;
         callback(iframe);
 };
 manipulateIframe('IFwinEdit_forms_dr4r3_forms_1371601293572', function (iframe) {
     console.log(iframe.body);
 });};

example

Answer

I had the same problem, in my case, I simply checked if onload function is fired or not.

var iframe = document.getElementById("someIframe");
var loadingStatus = true;
iframe.onload = function () {
    loadingStatus = false;
    //do whatever you want [in my case I wants to trigger postMessage]
};
if (loadingStatus)
    //do whatever you want [in my case I wants to trigger postMessage]
Answer

I think you should try using onreadystatechange event.

http://jsfiddle.net/fk8fc/3/

$(function () {
    var innerDoc = ($("#if")[0].contentDocument) ? $("#if")[0].contentDocument :   $("#if")[0].contentWindow.document;
    console.debug(innerDoc);
    $("#if").load( function () { 
        alert("load");
        alert(innerDoc.readyState) 
    });
    innerDoc.onreadystatechange = function () {
        alert(innerDoc.readyState) 
    };

    setTimeout(innerDoc.onreadystatechange, 5000);
});

EDIT: the context is not what I think it is. you can just check the readyState of iframe document and everything should be fine.

OP: This is a packaged up function I made from the concepts described above:

// runs a function after an iframe node's content has loaded
// note, this almost certainly won't work for frames loaded from a different domain
onReady: function(iframeNode, f) {
    var windowDocument = iframeNode[0].contentWindow.document;
    var iframeDocument = windowDocument?windowDocument : iframeNode[0].contentWindow.document
    if(iframeDocument.readyState === 'complete') {
        f();
    } else {
        iframeNode.load(f);
    }
}

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.