2

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.

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

12 Comments

It works, but only sometimes. Even with a timeout (on things that use JQuery), I occasionally get an error because something that uses JQuery can't find it. And the timeout solution isn't the best thing either, because its noticeable, one minute the page is text only, and then all of a sudden everything is styled and in position.
Any ideas why this happens? - sometimes it can find JQuery and sometimes it can't?
@sudoman: In that case, you'll have to hook the load and error events (before setting src) on each script, and wait to append subsequent nodes until you get the load or error callback. Not trivial, but not difficult.
@sudoman: Listening for both error and load, and I'd use addEventListener (although you could probably get away with onload and onerror), but yes. :-) Again be sure you hook the events before setting src, it's critical.
@sudoman: Not in any way that will mean anything, you're only hooking one-time events on script elements.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.