olfek
olfek

Reputation: 3520

Append string to head using JavaScript and execute

I have a JS script that I am using to append a string to the head tag, it works, but it doesn't execute what was appended.

HTML file:

<html>
    <head>
        ...
        <script src="test.js"></script>
        ...
    </head>
    <body>
        ...
    </body>
</html>

File: test.js

var str =
`
<script ... ></script>
<link...>
other stuff...
`

var html = document.getElementsByTagName('head')[0].innerHTML;
document.getElementsByTagName('head')[0].innerHTML=str+html;

With this, I understand that it would go into recursion if all of head was executed again just to execute the appended string (unless there is a way to execute only the appended string), but I have a way to deal with this. First, I need it to actually execute.

I have seen other similar questions, but they ask about appending a single script tag or something similar, I'm looking to add a whole chunk of HTML and have the browser execute it.

I'm looking for a pure JavaScript solution.

Note that str contains things like JQuery and Bootstrap and therefore the rest of the document heavily depends on this append & execution happening first.

Upvotes: 2

Views: 2728

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074178

Inserting scripts via innerHTML, insertAdjacentHTML(), etc. doesn't run them. To do that, we have to be a bit more creative; see inline comments:

setTimeout(() => {
  const str =
  `
  <script>console.log("Hello from script")<\/script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
  `;

  // Create an element outside the document to parse the string with
  const head = document.createElement("head");

  // Parse the string
  head.innerHTML = str;

  // Copy those nodes to the real `head`, duplicating script elements so
  // they get processed
  let node = head.firstChild;
  while (node) {
    const next = node.nextSibling;
    if (node.tagName === "SCRIPT") {
      // Just appending this element wouldn't run it, we have to make a fresh copy
      const newNode = document.createElement("script");
      if (node.src) {
        newNode.src = node.src;
      }
      while (node.firstChild) {
        // Note we have to clone these nodes
        newNode.appendChild(node.firstChild.cloneNode(true));
        node.removeChild(node.firstChild);
      }
      node = newNode;
    }
    document.head.appendChild(node);
    node = next;
  }
}, 800);
The appearance of <input type="button" value="this button"> changes when Bootstrap's CSS is loaded

The Bootstrap CSS is just there to demonstrate that the link is working. The timeout is just so you can see the link take effect.

Also note the backslash in the <\/script> tag in the string. We only need that because that string is within a <script>...</script> tag. We wouldn't need it if this were in a .js file.

Tested in current Chrome and Firefox. A version using a plain string instead of a template literal works in IE11, too.

We could do much the same with DOMParser instead of a freestanding head element. Instead of:

// Create an element outside the document to parse the string with
const head = document.createElement("head");

// Parse the string
head.innerHTML = str;

you'd use

// Parse the HTML and get the resulting head element.
// You could probably get away without the wrapper markup, but let's
// include it for completeness.
const parser = new DOMParser();
const doc = parser.parseFromString("<!doctype html><html><head>" + str + "</head></html>", "text/html");
const head = doc.head;

The rest is the same:

setTimeout(() => {
  const str =
  `
  <script>console.log("Hello from script")<\/script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
  `;

// Parse the HTML and get the resulting head element.
// You could probably get away without the wrapper markup, but let's
// include it for completeness.
const parser = new DOMParser();
const doc = parser.parseFromString("<!doctype html><html><head>" + str + "</head></html>", "text/html");
const head = doc.head;

  // Copy those nodes to the real `head`, duplicating script elements so
  // they get processed
  let node = head.firstChild;
  while (node) {
    const next = node.nextSibling;
    if (node.tagName === "SCRIPT") {
      // Just appending this element wouldn't run it, we have to make a fresh copy
      const newNode = document.createElement("script");
      if (node.src) {
        newNode.src = node.src;
      }
      while (node.firstChild) {
        // Note we have to clone these nodes
        newNode.appendChild(node.firstChild.cloneNode(true));
        node.removeChild(node.firstChild);
      }
      node = newNode;
    }
    document.head.appendChild(node);
    node = next;
  }
}, 800);
The appearance of <input type="button" value="this button"> changes when Bootstrap's CSS is loaded

Upvotes: 3

Related Questions