skip to Main Content

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

2

Answers


  1. 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.

    Login or Signup to reply.
  2. 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>`;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search