0

I use this to preview an e-mail

When I dynamically write HTML to an iframe somehow the <body> tag is omitted? The body tag holds the font-family etc. but now the whole tag is gone and the HTML document is not shown correctly

const iframe_content = $('#'+iframe_id).contents().find('html');
iframe_content.html(data.html);

contents of data.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="color-scheme" content="light dark">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>@media (prefers-color-scheme: dark){
.body_inner{background:#000 !important}
.content{border:0}
.content_inner{background:#000 !important;color:#fff !important}
}</style>
    </head>
    <body style="margin:0;padding:0;font-family:arial,helvetica,garuda">
        <div>first element</div>
    </body>
</html>

After the HTML is written to the iframe, the source of the iframe looks like this (the body tag is gone!)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="color-scheme" content="light dark">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>@media (prefers-color-scheme: dark){
.body_inner{background:#000 !important}
.content{border:0}
.content_inner{background:#000 !important;color:#fff !important}
}</style>
    </head>
    <div>first element</div>
</html>

I have tried to validate the HTML via https://validator.w3.org/ and no errors

clarkk
  • 27,151
  • 72
  • 200
  • 340
  • For your use case, why are you using an iframe if you are just filling it in with HTML from the source page? – imvain2 Apr 06 '23 at 16:26
  • Should be something with jQuery `.html(...)` function (if I do `$('html')[0].innerHTML = ...` the `` tag is there). Maybe we can investigate the source code? Also, do you start from an empty iframe, or does the iframe have some contents before you replace the html? – qrsngky Apr 06 '23 at 16:33
  • @imvain2 I use it to preview an e-mail – clarkk Apr 06 '23 at 16:34
  • Can you [convert html to data URI](https://stackoverflow.com/questions/9238890/convert-html-to-datatext-html-link-using-javascript) then use `iframe.src = ...`? – qrsngky Apr 06 '23 at 16:39
  • @qrsngky have tried but dosn't seem to work `$('#'+iframe_id)[0].src = 'data:text/html;charset=UTF-8,'+data.html;` – clarkk Apr 06 '23 at 16:53
  • @clarkk if you read [the accepted answer in the other thread](https://stackoverflow.com/a/9239272/4225384), you'll see that it's not just `'data:text/html;charset=UTF-8,' + data.html`, because you need to replace a number of special characters. Try `'data:text/html,' + encodeURIComponent(data.html);` – qrsngky Apr 09 '23 at 05:07
  • Use div tag instead of body tag. That's the simplest solution ;) – Sergio Apr 13 '23 at 19:05

2 Answers2

8

The reason is that jQuery's .html(value) method is not intended to be used for setting content with a (top-level) <html> tag. We can see this in the jQuery source code. When calling iframe_content.html(data.html);, the following calls are made inside the jQuery library:

html() calls access(), which calls (via callback) append(), which calls domManip(), which calls buildFragment(), which executes the following code, where elem is the data.html you passed as argument, and fragment is a documentFragment:

tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
// Deserialize a standard representation
tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
wrap = wrapMap[ tag ] || wrapMap._default;
tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];

So tmp is a <div> element (as appendChild returns the argument). tag is set to "html", and wrap to [0, "", ""], and jQuery.htmlPrefilter(elem) just returns elem. So in short, this is executed:

tmp.innerHTML = elem;

The problem is HTML does not allow a <div> element to have an <html> or <head> or <body> element as child, so the DOM (not jQuery) will not create those DOM elements as intended.

This is not the end of the jQuery processing, but already at this point we can see that information has been lost.

Work around

As jQuery's .html(value) method cannot do the job, bypass jQuery and set innerHTML directly. Replace this:

iframe_content.html(data.html);

with this:

iframe_content.get(0).innerHTML = data.html;

And now it will work.

trincot
  • 317,000
  • 35
  • 244
  • 286
0

Have you tried using a Shadow DOM?

All styles are encapsulated just like an iframe, but you also get the added benefits of naturally expanding content (compared to the fixed height of an iframe) and faster render times.

Example usage:

const shadow = someParentElement.attachShadow({
  mode: 'closed'
});
shadow.innerHTML = `<whatever>`;
cirex
  • 66
  • 6