Chap
Chap

Reputation: 3839

How to inject HTML into document.documentElement.innerHTML without closing the <body> tag

I have about 100 lines of boilerplate HTML that every file on my site begins with. It contains the following elements:

<!-- START OF BOILERPLATE TEXT -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta ...>
  <link href="..." rel="stylesheet">
  <script src="..."></script>
  <title>MY_TITLE</title>
</head>
<body>
  <div> Page banner ... </div>
  <hr>
<!-- END OF BOILERPLATE TEXT -->

I'd like to minimize the actual amount of boilerplate code needed for each page, and I've come up with the following solution:

<!-- Line 1 of actual code will load external script:

     <script src="/test/scripts/boot.js"></script> 

     For this demo I'm inlining it.   -->

<script>
  async function boot(title) {

    // Will insert header boilerplate in document.documentElement.
    // Actual code will fetch boilerplate code from server.

    const my_demo = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- ****************** page-specific <head> stuff ***************** -->
    <title>${title}</title>
  <!-- ****************** end page-specific stuff    ***************** -->
</head>
<body>
    <h1>Banner injected by boot.js</h1>
    <hr> <!-- end of page banner -->
    <!-- ****************** begin page content ***************** -->
`;

    try {
      document.documentElement.innerHTML = my_demo;
    } catch (my_error) {
      document.write('Error: ', my_error);
    }
  }
</script>

<!-- Line 2 of actual code will invoke boot() -->

<script>boot("Test Title")</script>

<!-- Now we begin adding our body html -->

<p>
  This is the actual content of the page. It should appear under the injected H1 and HR.
</p>

Here is the way I want it to look: Here is the way I want it to look:

The boot() script appears to correctly insert my <head> and banner html, but my "actual content" doesn't appear underneath. According to the Safari debugger, there was a </body> tag inserted after the last line inserted by boot(). How can I prevent that?

Update: It's important that the boilerplate HTML be injected first. The remainder of the content is sometimes very long, which causes nothing but unstyled text to appear for several seconds before the banner is painted and the text is restyled.

Upvotes: 0

Views: 87

Answers (2)

Montasser Mossallem
Montasser Mossallem

Reputation: 11

document.documentElement.innerHTML rewrites the entire HTML element, including the <head> and <body>. If you want to keep the existing contents, you can use insertAdjacentHTML. Here is the correct code:

async function boot(title) {
  const headContent = `
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>${title}</title>
  `;

  const bodyContent = `
    <h1>Injected by boot.js</h1>
    <hr> <!-- end of page header -->
  `;

  // Insert into head
  document.head.insertAdjacentHTML('beforeend', headContent);
  
  // Insert into body without replacing existing content
  document.body.insertAdjacentHTML('afterbegin', bodyContent);
}

boot("Test Title");

Upvotes: 0

Ray Wallace
Ray Wallace

Reputation: 1942

When you insert text into the DOM (your .innerHTML = my_demo), the browser will close off any elements in order to keep the DOM as correct as it can. Which is why you are seeing a </body> that you did not put there.

Here is an example of doing what I think you are asking, by surrounding your real HTML body in an element (<main> in my example) with an id unique enough not to be found in your HTML otherwise. "MainHtmlToBeAppended" in my example.

Then you can retrieve your HTML and include with your my_demo string when setting .innerHTML.

I use "defer" in the script tag to ensure the DOM is loaded before trying to access it.

Note that I moved the boot("Test Title") invocation into the .js file, but you could leave it in your html file if you prefer. It just keeps the "extra" content in your .html file to a minimal single <script> line.

boot("Test Title")


function boot(title) {

    // Will insert header boilerplate in document.documentElement.
    // Actual code will fetch boilerplate code from server.

    const my_demo = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- ****************** page-specific <head> stuff ***************** -->
    <title>${title}</title>
  <!-- ****************** end page-specific stuff    ***************** -->
</head>
<body>
    <h1>Injected by boot.js</h1>
    <hr> <!-- end of page header -->
    <!-- ****************** begin page content ***************** -->
`;

    // EDIT Get the "real" content
    let mainHtml = document.querySelector( "#MainHtmlToBeAppended" ).innerHTML

    try {
      // EDIT Append the "real" content with the my_demo string
      // document.documentElement.innerHTML = my_demo;
      document.documentElement.innerHTML = my_demo + mainHtml;
    } catch (my_error) {
      // EDIT Better to set .innerHTML than .write()
      // document.write('Error: ', my_error);
      document.documentElement.innerHTML = `MY ERROR: ${my_error}`;
    }
}
<!-- Note: test.js would contain the Javascript shown in the snippet -->
<script defer src="test.js"> </script>

<!-- Now we begin adding our body html -->

<main id="MainHtmlToBeAppended">
  <p>
    This is the actual content of the page. It should appear under the injected H1 and HR.
  </p>
</main>

Upvotes: 1

Related Questions