How to detect when an iframe has already been loaded
Asked Answered
M

8

40

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?

Matildamatilde answered 18/6, 2013 at 0:31 Comment(5)
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?Laureen
Looks like the same question as thisElma
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.Matildamatilde
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.Matildamatilde
As of jQuery 3.0, load is gone. Please try .on('load', function() { ... }) instead.Gingersnap
O
55

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.

Osbourne answered 22/3, 2016 at 13:23 Comment(7)
Does this require a dependency on 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?Peephole
@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.Osbourne
@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.Osbourne
is this still the best approach in 2020? very frustrating that $.ready doesn't work properly for iframes.Bautista
@Bautista 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.Wayland
This solution works for most iframes, but for more complex iframes like RTE editors (e.g., YUI 2 Rich Text Editor 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.Expiratory
The reference given for "Using .ready() on an iframe element isn't supported" is a good one. Is there a weakness with the solution from comment 2? Reduces to $(document).ready(function() { if (window.frameElement) { $(parent.document).ready(function() { // ... which I believe you're saying does not work? Why would that solution look successful?Floriaflorian
T
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.

Theocritus answered 18/6, 2013 at 7:29 Comment(1)
This is exactly the kind of thing I want to avoid, tho i agree it would work.Matildamatilde
S
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://mcmap.net/q/136813/-javascript-callback-when-iframe-is-finished-loading answer for explanation.

Subsidy answered 24/10, 2021 at 7:58 Comment(0)
B
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]
Bibulous answered 28/1, 2019 at 9:46 Comment(2)
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.Matildamatilde
@BT is right. The onload handler won't fire if the iframe has already been loaded.Bautista
M
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.

Matildamatilde answered 27/6, 2013 at 18:54 Comment(1)
@B T See my answerOsbourne
O
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>
Oarlock answered 29/7, 2023 at 11:11 Comment(0)
A
-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

Antipasto answered 18/6, 2013 at 7:23 Comment(6)
Sorry, I just tested this and this fires before the iframe is finished loadingMatildamatilde
Do you want the content inside the iframe to finish loading first?Antipasto
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.Erasme
So define loaded. w/o images it is ready. otherwise i suggest postMessage like @Adam suggestedAntipasto
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.Osbourne
@Antipasto There's a better way, see my answer.Osbourne
G
-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);
    }
}
Goodyear answered 19/6, 2013 at 23:8 Comment(6)
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.Matildamatilde
Why do you try to get contentDocument and fall back to using contentWindow.document . Why not use contentWindow.document every time?Matildamatilde
This is the suggested code to get iFrame document. Probably to support old browser. And also jQuery might have some short cut for it.Goodyear
Ah gotcha. FYI this stuff worked when loading from a webserver. Thanks again!Matildamatilde
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.Matildamatilde
For example, on chrome, the ready state is set to complete before the page is really fully loaded.Matildamatilde

© 2022 - 2024 — McMap. All rights reserved.