189

I'm trying to create an iframe from JavaScript and fill it with arbitrary HTML, like so:

var html = '<body>Foo</body>';
var iframe = document.createElement('iframe');
iframe.src = 'data:text/html;charset=utf-8,' + encodeURI(html);

I would expect iframe to then contain a valid window and document. However, this isn't the case:

> console.log(iframe.contentWindow);
null

Try it for yourself: http://jsfiddle.net/TrevorBurnham/9k9Pe/

What am I overlooking?

Trevor Burnham
  • 76,828
  • 33
  • 160
  • 196
  • 10
    Note that HTML5 introduced a new parameter doing this automatically: http://www.w3schools.com/tags/att_iframe_srcdoc.asp **The only problem is the browser compatibility...** – Vincent Audebert Dec 11 '13 at 20:51
  • possible duplicate of [putting html inside an iframe (using javascript)](http://stackoverflow.com/questions/620881/putting-html-inside-an-iframe-using-javascript) – Ciro Santilli OurBigBook.com Feb 16 '14 at 21:36

8 Answers8

281

Allthough your src = encodeURI should work, I would have gone a different way:

var iframe = document.createElement('iframe');
var html = '<body>Foo</body>';
document.body.appendChild(iframe);
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(html);
iframe.contentWindow.document.close();

As this has no x-domain restraints and is completely done via the iframe handle, you may access and manipulate the contents of the frame later on. All you need to make sure of is, that the contents have been rendered, which will (depending on browser type) start during/after the .write command is issued - but not nescessarily done when close() is called.

A 100% compatible way of doing a callback could be this approach:

<html><body onload="parent.myCallbackFunc(this.window)"></body></html>

Iframes has the onload event, however. Here is an approach to access the inner html as DOM (js):

iframe.onload = function() {
   var div=iframe.contentWindow.document.getElementById('mydiv');
};
mschr
  • 8,531
  • 3
  • 21
  • 35
  • Interesting; I hadn't seen this technique before. I know that the URI encoding/decoding adds a performance hit, yet I've also seen it described as the sole alternative in environments that don't support the [`srcdoc` property](https://developer.mozilla.org/en/HTML/Element/iframe#attr-srcdoc). Is there a downside to the `document.write` approach? – Trevor Burnham May 03 '12 at 15:34
  • i think not, and it is the most backwards-compatible solution i can come up with in regards to browser compatibility. There is a quirk as to how you access the contentDocument, but i believe the above should work everywhere (IE too) - well, `document.bodý` is hack to append but otherwise =) If you run into security restrictions, set SRC to about:blank - as document operations are not allowed on x-domain, as wiw anything else in DOM – mschr May 03 '12 at 16:14
  • 1
    @mschr Does this method support full HTML page code where includes and stylesheets are loaded as well? See http://stackoverflow.com/questions/19871886/ – 1.21 gigawatts Nov 09 '13 at 03:49
  • 2
    Basically, yes i believe it should, ill comment there. The technique basically opens an inputtextstream which you may write to via document.write. In turn, a normal loading webpage retreives this through a websocket-stream. – mschr Nov 12 '13 at 21:56
  • 2
    This works in Internet Explorer! That is handy since data URIs cannot be used as iframe sources in any version of IE. http://caniuse.com/#feat=datauri – Jesse Hallett Mar 18 '14 at 01:34
  • Isn't a downside that you have to convert HTML to a string and therefore lost event listeners etc. – Nick Manning Oct 18 '14 at 01:15
  • Hi Nick; Well listeners are not lost, as they will get re-interpreted. I.e `foo` will infact fire an onload event. If you copy existing html that does not have inline event markup, you will ofc. have to reinitialise (read: write to document) the javascript which binds events. – mschr Nov 24 '14 at 11:39
  • This code doesnt work with this simple html var html = ''; – MayurB Jan 06 '15 at 11:22
  • You sure?? Only just know I tested your sample html in chrome, IE and FF with success. Be careful whilst writing inline javascript in a .html file - you will need to separate the '<' and '/' in closing tag. Otherwise some browsers thinks the scripts ends there (disregarding hyphens/quotes)... var html = ' – mschr Jan 06 '15 at 13:23
  • I used `
    Foo
    ` as the HTML and when I tried to retrieve the div by id using `document.getElementById("fooid")`, I got nothing. Any idea why?
    – haridsv Jan 30 '15 at 18:23
  • https://bugzilla.mozilla.org/show_bug.cgi?id=663406 maybe a bug has resurfaced.. tried putting the script on a html file and directing the browser to its url? also, make sure the youre putting content into is present before calling document.appendChild – mschr Mar 08 '15 at 14:59
  • How can I load a dynamic url instead of static HTML? – Harikrishnan Mar 18 '16 at 06:51
  • Is there a way to disallow the iFrame from accessing the parent page? By default the iFrame can access top.* and gain full access to the parent page. Is there a way to sandbox it? – tinkerr Jul 29 '16 at 23:39
  • 2
    Interesting that the `document.body.appendChild(iframe)` is required for this to work. – Paul May 13 '17 at 16:59
  • The instant, that `document.body.appendChild(iframe)` is called, the previously buffered html page (in the iframe.DOMDocument, i.e. its just abunch of linked-list elements - not rendered html text) gets flushed out into the HTML rendering engine. Some browsers additionally have required the `iframe` element to be `visible` – mschr Jun 10 '17 at 11:11
  • FYI, `iframe.contentWindow.document` is equivalent to `iframe.contentDocument`. [See here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#Scripting) – Yonggoo Noh Apr 27 '18 at 05:17
  • Sorry I am having trouble understanding what you meant when you said that you have to make sure the content is rendered. How can you make sure it is rendered? Is there an additional step that you have to do to render it? Isn't the code you provided enough to render the content? – Cave Johnson Oct 29 '18 at 21:56
  • Any idea why an iframe created like this would lose its content when cloned? Seems that if you clone and iframe the content written to it this way is lost on the clone. – Craig Harshbarger Jul 22 '19 at 21:55
  • Careful, `iframe.contentWindow` cannot be accessed until loaded, even though an empty src should load immediately. I would suggest to use: `iframe.onload = function(){var d = this.contentWindow.document;d.open();d.write(' hello world');d.close();};` This way you can write the code before even adding the dynamically created `iframe` to the document. – Yeti Oct 11 '19 at 02:04
  • works fine in firefox, even jquery contents() method does not work, this is the only way – webjockey Feb 13 '20 at 07:22
152

Setting the src of a newly created iframe in javascript does not trigger the HTML parser until the element is inserted into the document. The HTML is then updated and the HTML parser will be invoked and process the attribute as expected.

http://jsfiddle.net/9k9Pe/2/

var iframe = document.createElement('iframe');
var html = '<body>Foo</body>';
iframe.src = 'data:text/html;charset=utf-8,' + encodeURI(html);
document.body.appendChild(iframe);
console.log('iframe.contentWindow =', iframe.contentWindow);

Also this answer your question it's important to note that this approach has compatibility issues with some browsers, please see the answer of @mschr for a cross-browser solution.

GillesC
  • 10,647
  • 3
  • 40
  • 55
  • 3
    Doesn't even work in IE10. @mschr's answer works in IE7+ for sure, maybe even older versions. – James M. Greene Oct 23 '13 at 18:28
  • 6
    His question was "What am I overlooking?" and that was the fact that is iframe wasn't appended to the document. I never claim it was cross browsers and it's only over a year after I answered that someone actually complained. No it's not cross browser. But let's be honest if you want code quality there is probably a much cleaner solution than using a iframe in the first place :) – GillesC Nov 22 '13 at 15:10
  • 2
    Note that [`encodeURI(...)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) doesn't encode all characters, so if your HTML has special characters like `#`, this will break. [`encodeURIComponent(...)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) encodes these characters. See [this answer](https://stackoverflow.com/questions/9238890/convert-html-to-datatext-html-link-using-javascript) – Ryan Morlok Feb 19 '20 at 16:25
23

I know this is an old question but I thought I would provide an example using the srcdoc attribute as this is now widely supported and this is question is viewed often.

Using the srcdoc attribute, you can provide inline HTML to embed. It overrides the src attribute if supported. The browser will fall back to the src attribute if unsupported.

I would also recommend using the sandbox attribute to apply extra restrictions to the content in the frame. This is especially important if the HTML is not your own.

const iframe = document.createElement('iframe');
const html = '<body>Foo</body>';
iframe.srcdoc = html;
iframe.sandbox = '';
document.body.appendChild(iframe);

If you need to support older browsers, you can check for srcdoc support and fallback to one of the other methods from other answers.

function setIframeHTML(iframe, html) {
  if (typeof iframe.srcdoc !== 'undefined') {
    iframe.srcdoc = html;
  } else {
    iframe.sandbox = 'allow-same-origin';
    iframe.contentWindow.document.open();
    iframe.contentWindow.document.write(html);
    iframe.contentWindow.document.close();
  }
}

var iframe = document.createElement('iframe');
iframe.sandbox = '';
var html = '<body>Foo</body>';

document.body.appendChild(iframe);
setIframeHTML(iframe, html);
frobinsonj
  • 1,109
  • 9
  • 21
  • 1
    But what if a new element does not need to be created? If an iframe already exists and the data is just to be updated? – NL23codes Aug 13 '20 at 01:46
  • This use can use postMessage to send a message to the JavaScript within the iframe describing what changes need to happen, then the iframe can update itself with its own code. Bit tricky, but I think it is the only way. – clinux Sep 21 '21 at 00:04
17

There is an alternative for creating an iframe whose contents are a string of HTML: the srcdoc attribute. This is not supported in older browsers (chief among them: Internet Explorer, and possibly Safari?), but there is a polyfill for this behavior, which you could put in conditional comments for IE, or use something like has.js to conditionally lazy load it.

zedd45
  • 2,101
  • 1
  • 31
  • 34
  • 2
    support for this is pretty mainstream now (save IE). And this is definitely preferable to accessing contentDocument directly - especially since if used in conjunction with the sandbox attribute, you can't access contentDocument. – CambridgeMike Nov 06 '14 at 18:27
17

Thanks for your great question, this has caught me out a few times. When using dataURI HTML source, I find that I have to define a complete HTML document.

See below a modified example.

var html = '<html><head></head><body>Foo</body></html>';
var iframe = document.createElement('iframe');
iframe.src = 'data:text/html;charset=utf-8,' + encodeURI(html);

take note of the html content wrapped with <html> tags and the iframe.src string.

The iframe element needs to be added to the DOM tree to be parsed.

document.body.appendChild(iframe);

You will not be able to inspect the iframe.contentDocument unless you disable-web-security on your browser. You'll get a message

DOMException: Failed to read the 'contentDocument' property from 'HTMLIFrameElement': Blocked a frame with origin "http://localhost:7357" from accessing a cross-origin frame.

kylewelsby
  • 4,031
  • 2
  • 29
  • 35
9

The URL approach will only work for small HTML fragements. The more solid approach is to generate an object URL from a blob and use it as a source of the dynamic iframe.

const html = '<html>...</html>';
const iframe = document.createElement('iframe');
const blob = new Blob([html], {type: 'text/html'});
iframe.src = window.URL.createObjectURL(blob);
document.body.appendChild(iframe);
damoeb
  • 154
  • 3
  • 9
  • This approach has a problem that the frame's window.location.href will be something like "blob:https://example.com/83f3ac60-2cd9-42dd-b430-e1c09484009e" – Sych Nov 23 '21 at 13:23
0

Do this

...
var el = document.getElementById('targetFrame');

var frame_win = getIframeWindow(el);

console.log(frame_win);
...

getIframeWindow is defined here

function getIframeWindow(iframe_object) {
  var doc;

  if (iframe_object.contentWindow) {
    return iframe_object.contentWindow;
  }

  if (iframe_object.window) {
    return iframe_object.window;
  } 

  if (!doc && iframe_object.contentDocument) {
    doc = iframe_object.contentDocument;
  } 

  if (!doc && iframe_object.document) {
    doc = iframe_object.document;
  }

  if (doc && doc.defaultView) {
   return doc.defaultView;
  }

  if (doc && doc.parentWindow) {
    return doc.parentWindow;
  }

  return undefined;
}
Dominique Fortin
  • 2,212
  • 15
  • 20
-2

(function(){

var frame = document.createElement('iframe');
frame.src = 'https://1zr2h9xgfxqt.statuspage.io/embed/frame';
frame.style.position = 'fixed';
frame.style.border = 'none';
frame.style.boxShadow = '0 20px 32px -8px rgba(9,20,66,0.25)';
frame.style.zIndex = '9999';
frame.style.transition = 'left 1s ease, bottom 1s ease, right 1s ease';

var mobile;
if (mobile = screen.width < 450) {
  frame.src += '?mobile=true';
  frame.style.height = '20vh';
  frame.style.width = '100vw';
  frame.style.left = '-9999px';
  frame.style.bottom = '-9999px';
  frame.style.transition = 'bottom 1s ease';
} else {
  frame.style.height = '115px';
  frame.style.width = '320px';
  frame.style.left = 'auto';
  frame.style.right = '-9999px';
  frame.style.bottom = '60px';
}

document.body.appendChild(frame);

var actions = {
  showFrame: function() {
    if (mobile) {
      frame.style.left = '0';
      frame.style.bottom = '0';
    }
    else {
      frame.style.left = 'auto';
      frame.style.right = '60px'
    }
  },
  dismissFrame: function(){
    frame.style.left = '-9999px';
  }
}

window.addEventListener('message', function(event){
  if (event.data.action && actions.hasOwnProperty(event.data.action)) {
    actions[event.data.action](event.data);
  }
}, false);

window.statusEmbedTest = actions.showFrame;

})();