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.

olfek
  • 3,210
  • 4
  • 33
  • 49

1 Answers1

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
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 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. – olfek Apr 04 '17 at 19:42
  • Any ideas why this happens? - sometimes it can find `JQuery` and sometimes it can't? – olfek Apr 04 '17 at 20:01
  • @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. – T.J. Crowder Apr 04 '17 at 20:35
  • @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. – T.J. Crowder Apr 04 '17 at 20:46
  • will having an event listener for each node added affect performance at all? – olfek Apr 04 '17 at 20:50
  • @sudoman: "hook the event" = attach a handler for the event. E.g., using `addEventListener` or setting `onxyz`. – T.J. Crowder Apr 04 '17 at 20:50
  • 1
    @sudoman: Not in any way that will mean anything, you're only hooking one-time events on `script` elements. – T.J. Crowder Apr 04 '17 at 20:50
  • A ` – olfek Apr 05 '17 at 13:03
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/139979/discussion-between-sudoman-and-t-j-crowder). – olfek Apr 05 '17 at 13:05
  • @sudoman: Yes, DOMContentLoaded would have fired long, long before. As will the `window` `load` event. – T.J. Crowder Apr 05 '17 at 14:03
  • I've managed to kind of get a work around going by dispatching my own event when the loading completes and then doing that `script` tag, but for some reason I get `Uncaught DOMException: Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched`. If i add a timeout, this error message goes away and all is good, why does this happen? – olfek Apr 05 '17 at 14:09
  • @sudoman: I don't know (though the message is fairly clear, could you be firing this in a handler for DOMContentLoaded?). At this point, we're far afield from the original question. I suggest packaging up the issue you're having in an MCVE and posting a question about it. – T.J. Crowder Apr 05 '17 at 14:13