I am trying to replace the whole DOM on page load to do a no-js fallback for a user created knockout page.
I have it replacing the DOM, but when I do the scripts included in the new document aren’t running. I was wondering if theres any way of forcing them to run.
<!DOCTYPE html> <html> <head> <title>Title1</title> </head> <body> Hello world <!-- No JS enabled content --> </body> <script type="text/javascript"> var model = { 'template' : 'u003chtmlu003eu003cheadu003eu003ctitleu003eTitle2u003c/titleu003eu003cscript type="text/javascript"u003ealert("test");u003c/scriptu003eu003c/headu003eu003cbodyu003eHello world2u003c/bodyu003eu003c/htmlu003e' }; document.documentElement.innerHTML = model.template; </script> </html>
template contains the following encoded
<html> <head> <title>aaa</title> <script type='text/javascript'>alert('hello world');</script> </head> <body> Hello world <!-- JS enabled content --> </body> </html>
how can I get the alert to run?
Advertisement
Answer
As you’ve discovered, the code in the script
tags in the text you assign to innerHTML
is not executed. Interestingly, though, on every browser I’ve tried, the script
elements are created and placed in the DOM.
This means it’s easy to write a function to run them, in order, and without using eval
and its weird effect on scope:
function runScripts(element) { var scripts; // Get the scripts scripts = element.getElementsByTagName("script"); // Run them in sequence (remember NodeLists are live) continueLoading(); function continueLoading() { var script, newscript; // While we have a script to load... while (scripts.length) { // Get it and remove it from the DOM script = scripts[0]; script.parentNode.removeChild(script); // Create a replacement for it newscript = document.createElement('script'); // External? if (script.src) { // Yes, we'll have to wait until it's loaded before continuing newscript.onerror = continueLoadingOnError; newscript.onload = continueLoadingOnLoad; newscript.onreadystatechange = continueLoadingOnReady; newscript.src = script.src; } else { // No, we can do it right away newscript.text = script.text; } // Start the script document.documentElement.appendChild(newscript); // If it's external, wait for callback if (script.src) { return; } } // All scripts loaded newscript = undefined; // Callback on most browsers when a script is loaded function continueLoadingOnLoad() { // Defend against duplicate calls if (this === newscript) { continueLoading(); } } // Callback on most browsers when a script fails to load function continueLoadingOnError() { // Defend against duplicate calls if (this === newscript) { continueLoading(); } } // Callback on IE when a script's loading status changes function continueLoadingOnReady() { // Defend against duplicate calls and check whether the // script is complete (complete = loaded or error) if (this === newscript && this.readyState === "complete") { continueLoading(); } } } }
Naturally the scripts can’t use document.write
.
Note how we have to create a new script
element. Just moving the existing one elsewhere in the document doesn’t work, it’s been marked by the browser as having been run (even though it wasn’t).
The above will work for most people using innerHTML
on an element somewhere in the body of the document, but it won’t work for you, because you’re actually doing this on the document.documentElement
. That means the NodeList
we get back from this line:
// Get the scripts scripts = element.getElementsByTagName("script");
…will keep expanding as we add further scripts to the document.documentElement
. So in your particular case, you have to turn it into an array first:
var list, scripts, index; // Get the scripts list = element.getElementsByTagName("script"); scripts = []; for (index = 0; index < list.length; ++index) { scripts[index] = list[index]; } list = undefined;
…and later in continueLoading
, you have to manually remove entries from the array:
// Get it and remove it from the DOM script = scripts[0]; script.parentNode.removeChild(script); scripts.splice(0, 1); // <== The new line
Here’s a complete example for most people (not you), including the scripts doing things like function declarations (which would be messed up if we used eval
): Live Copy | Live Source
<!DOCTYPE html> <html> <head> <meta charset=utf-8 /> <title>Run Scripts</title> </head> <body> <div id="target">Click me</div> <script> document.getElementById("target").onclick = function() { display("Updating div"); this.innerHTML = "Updated with script" + "<div id='sub'>sub-div</div>" + "<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js'></scr" + "ipt>" + "<script>" + "display('Script one run');" + "function foo(msg) {" + " display(msg); " + "}" + "</scr" + "ipt>" + "<script>" + "display('Script two run');" + "foo('Function declared in script one successfully called from script two');" + "$('#sub').html('updated via jquery');" + "</scr" + "ipt>"; runScripts(this); }; function runScripts(element) { var scripts; // Get the scripts scripts = element.getElementsByTagName("script"); // Run them in sequence (remember NodeLists are live) continueLoading(); function continueLoading() { var script, newscript; // While we have a script to load... while (scripts.length) { // Get it and remove it from the DOM script = scripts[0]; script.parentNode.removeChild(script); // Create a replacement for it newscript = document.createElement('script'); // External? if (script.src) { // Yes, we'll have to wait until it's loaded before continuing display("Loading " + script.src + "..."); newscript.onerror = continueLoadingOnError; newscript.onload = continueLoadingOnLoad; newscript.onreadystatechange = continueLoadingOnReady; newscript.src = script.src; } else { // No, we can do it right away display("Loading inline script..."); newscript.text = script.text; } // Start the script document.documentElement.appendChild(newscript); // If it's external, wait for callback if (script.src) { return; } } // All scripts loaded newscript = undefined; // Callback on most browsers when a script is loaded function continueLoadingOnLoad() { // Defend against duplicate calls if (this === newscript) { display("Load complete, next script"); continueLoading(); } } // Callback on most browsers when a script fails to load function continueLoadingOnError() { // Defend against duplicate calls if (this === newscript) { display("Load error, next script"); continueLoading(); } } // Callback on IE when a script's loading status changes function continueLoadingOnReady() { // Defend against duplicate calls and check whether the // script is complete (complete = loaded or error) if (this === newscript && this.readyState === "complete") { display("Load ready state is complete, next script"); continueLoading(); } } } } function display(msg) { var p = document.createElement('p'); p.innerHTML = String(msg); document.body.appendChild(p); } </script> </body> </html>
And here’s your fiddle updated to use the above where we turn the NodeList
into an array:
HTML:
<body> Hello world22 </body>
Script:
var model = { 'template': 'tu003chtmlu003ernttu003cheadu003erntttu003ctitleu003eaaau003c/titleu003erntttu003cscript src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.1/jquery.min.js"u003eu003c/scriptu003erntttu003cscript type=u0027text/javascriptu0027u003ealert($(u0027bodyu0027).html());u003c/scriptu003ernttu003c/headu003ernttu003cbodyu003erntttHello worldrnttu003c/bodyu003erntu003c/htmlu003e' }; document.documentElement.innerHTML = model.template; function runScripts(element) { var list, scripts, index; // Get the scripts list = element.getElementsByTagName("script"); scripts = []; for (index = 0; index < list.length; ++index) { scripts[index] = list[index]; } list = undefined; // Run them in sequence continueLoading(); function continueLoading() { var script, newscript; // While we have a script to load... while (scripts.length) { // Get it and remove it from the DOM script = scripts[0]; script.parentNode.removeChild(script); scripts.splice(0, 1); // Create a replacement for it newscript = document.createElement('script'); // External? if (script.src) { // Yes, we'll have to wait until it's loaded before continuing newscript.onerror = continueLoadingOnError; newscript.onload = continueLoadingOnLoad; newscript.onreadystatechange = continueLoadingOnReady; newscript.src = script.src; } else { // No, we can do it right away newscript.text = script.text; } // Start the script document.documentElement.appendChild(newscript); // If it's external, wait if (script.src) { return; } } // All scripts loaded newscript = undefined; // Callback on most browsers when a script is loaded function continueLoadingOnLoad() { // Defend against duplicate calls if (this === newscript) { continueLoading(); } } // Callback on most browsers when a script fails to load function continueLoadingOnError() { // Defend against duplicate calls if (this === newscript) { continueLoading(); } } // Callback on IE when a script's loading status changes function continueLoadingOnReady() { // Defend against duplicate calls and check whether the // script is complete (complete = loaded or error) if (this === newscript && this.readyState === "complete") { continueLoading(); } } } } runScripts(document.documentElement);
This approach just occurred to me today when reading your question. I’ve never seen it used before, but it works in IE6, IE8, Chrome 26, Firefox 20, and Opera 12.15.