39

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?

B T
  • 57,525
  • 34
  • 189
  • 207
  • 1
    Is the iframe in the same domain as the page that wants to know when it's loaded? Do you control the content in the iframe? or is the iframe content known? – jfriend00 Jun 18 '13 at 00:53
  • Looks like the same question as [this](http://stackoverflow.com/questions/164085/javascript-callback-when-iframe-is-finished-loading?rq=1) –  Jun 18 '13 at 01:02
  • So as a matter of fact, it is in the same domain. Of course, the best answer would be the same for both. Ideally, I don't want one page to be modifying content in the other page, or checking variables in another page. – B T Jun 18 '13 at 01:28
  • 1
    Mike W - That question doesn't mention how to do this if the iframe is / may be already loaded. This is the main question for me. – B T Jun 18 '13 at 01:34
  • As of jQuery 3.0, `load` is gone. Please try `.on('load', function() { ... })` instead. – Doppelganger Sep 15 '16 at 11:56

8 Answers8

53

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.

dude
  • 5,678
  • 11
  • 54
  • 81
  • Does this require a dependency on https://markjs.io/'s jqueryMark plugin? I'm trying to understand what is the purpose of listening for `load.jqueryMark` inside of `observeOnload`? I can see that if the iframe content's src is "about:blank" it's going to call `observeOnload` and wait for jqueryMark...but why jqueryMark? – Wesley Musgrove Apr 10 '17 at 18:06
  • @WesleyMusgrove The `observerOnload` function and `load` event are two different things. The `load.jqueryMark` is just a namespace, that makes it possible to unsubscribe after a `load` was fired. It's not necessary to have mark.js included, there's no dependency. However, I'd recommend to use the latest code from the [repository](https://github.com/julmot/mark.js/). – dude Apr 11 '17 at 14:18
  • @HotN There's no chance to access an iframe from a different origin unless you install a browser extension like CORS. Therefore the script implements a fallback. – dude Mar 16 '18 at 16:37
  • is this still the best approach in 2020? very frustrating that $.ready doesn't work properly for iframes. – Crashalot May 23 '20 at 06:23
  • 1
    @Crashalot I agree it is frustrating that a cleaner solution hasn't presented itself. If you or dude or anyone finds a better solution please add an answer and link to it here. – RedSands Jun 04 '20 at 14:57
  • This solution works for most iframes, but for more complex iframes like RTE editors (e.g., [YUI 2 Rich Text Editor](https://yui.github.io/yui2/docs/yui_2.9.0_full/editor/index.html) it may not be truly "done" when it returns a completed state because the widget is still setting up the iframe document body dynamically. I haven't found a 100% reliable way to truly wait until an iframe-based RTE widget is ready except to wait for N number of ticks without a DOM update to the iframe document, which can be different for each widget. – thdoan Jan 13 '22 at 23:44
3

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.

Adam Bergmark
  • 7,316
  • 3
  • 20
  • 23
  • 1
    This is exactly the kind of thing I want to avoid, tho i agree it would work. – B T Jun 19 '13 at 00:16
2

This function will run your callback function immediately if the iFrame is already loaded or wait until the iFrame is completely loaded before running your callback function. Just pass in your callback function that you want to run when the iFrame finishes loading and the element to this function:

function iframeReady(callback, iframeElement) {
    const iframeWindow = iframeElement.contentWindow;
    if ((iframeElement.src == "about:blank" || (iframeElement.src != "about:blank" && iframeWindow.location.href != "about:blank")) && iframeWindow.document.readyState == "complete") {
        callback();
    } else {
        iframeWindow.addEventListener("load", callback);
    }
}

This will take care of the most common issues like chrome initializing iframe with about:blank and iFrame not supporting DOMContentLoaded event. See this https://stackoverflow.com/a/69694808/15757382 answer for explanation.

arafatgazi
  • 341
  • 4
  • 5
1

I had the same problem. In my case, I simply checked if the 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]
Crashalot
  • 33,605
  • 61
  • 269
  • 439
Mahyar Zarifkar
  • 176
  • 1
  • 12
  • 5
    I'm pretty sure that won't work if you set the onload handler after the iframe has been loaded, which is the whole point of the OP. – B T Feb 08 '19 at 21:31
  • @BT is right. The `onload` handler won't fire if the iframe has already been loaded. – Crashalot May 24 '20 at 22:03
0

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.

B T
  • 57,525
  • 34
  • 189
  • 207
0

You may use ownerDocument to get Document data once iframe was loaded

<iframe src="http://localhost:8080/all-videos" frameborder="0"></iframe>
 <script>
    var ifv = document.querySelector('iframe');

    ifv.onload = function() {
        console.log(this.ownerDocument.body);
    }
</script>
-1

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

raam86
  • 6,785
  • 2
  • 31
  • 46
  • Sorry, I just tested this and this fires before the iframe is finished loading – B T Jun 19 '13 at 00:15
  • Do you want the content **inside** the iframe to finish loading first? – raam86 Jun 19 '13 at 00:18
  • `window.onload` assumes that you also want to wait for images and other iframes inside the window. What we want is only to check if that specific iframe was loaded. – user3631654 Mar 21 '16 at 09:16
  • So define loaded. w/o images it is ready. otherwise i suggest postMessage like @Adam suggested – raam86 Mar 21 '16 at 10:14
  • That would mean that we can change the contents of the iframe. But what if for example I am embedding just an iframe that comes from a third party? This can't be the solution for all cases. – dude Mar 21 '16 at 15:11
  • @raam86 There's a better way, see my answer. – dude Mar 23 '16 at 06:17
-1

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);
    }
}
B T
  • 57,525
  • 34
  • 189
  • 207
Onur Topal
  • 3,042
  • 1
  • 24
  • 41
  • Looks like this gets really close, so far, in my testing. I've gotten it to work in IE and Firefox. Chrome is yelling about the same-origin policy for files both loaded from my filesystem. I'm going to test with a webserver to see if it works there. – B T Jun 20 '13 at 20:37
  • Why do you try to get contentDocument and fall back to using contentWindow.document . Why not use contentWindow.document every time? – B T Jun 20 '13 at 20:50
  • This is the suggested code to get iFrame document. Probably to support old browser. And also jQuery might have some short cut for it. – Onur Topal Jun 21 '13 at 09:01
  • Ah gotcha. FYI this stuff worked when loading from a webserver. Thanks again! – B T Jun 21 '13 at 21:15
  • Hey, so actually, in further testing, this doesn't quite work in certain cases for various browsers. This gives me good ideas, but there are a few problems with this. – B T Jun 26 '13 at 18:43
  • 4
    For example, on chrome, the ready state is set to complete before the page is really fully loaded. – B T Jun 26 '13 at 18:44