Embedded script tags in content loaded through ajax (and execute the script tags dynamically)

I’ve struggled in Griffin.Yo to be able to get script tags to load and execute in the correct order. This article will explain how you can achieve that.

My original problem was that the JavaScript in my script tags was not executed when I attached a HTML partial to the main page. I had a partial with the following code:

<!-- some view elements here -->

<script src="/Scripts/Griffin.Editor.js" type="text/javascript"></script>
<script type="text/javascript">
            marked.setOptions({
                renderer: new marked.Renderer(),
                gfm: true,
                tables: true,
                breaks: false,
                pedantic: false,
                sanitize: true,
                smartLists: true,
                smartypants: false
            });
            var textParser = {
                parse: function (text) {
                    return marked(text);
                }
            }
            var prismHighlighter = {
                highlight: function (blockElements, inlineElements) {
                    blockElements.forEach(function(item) {
                        Prism.highlightElement(item);
                    });

                }
            };
            var editor = new Griffin.Editor('editor', textParser);
            editor.syntaxHighlighter = prismHighlighter;
            editor.preview();
</script>

The referenced script was not loaded and the embedded script was not executed. To try to solve that I created a small script containing something like this:

[javascript]
var scripts = viewElem.getElementsByTagName(‘script’);
for (let i = 0; i < len; i++) {
var scriptTag = scripts[0];
let node = document.createElement(‘script’);
if (scriptTag.src && scriptTag.src.length > 0) {
node.src = scriptTag.src;
node.type = scriptTag.type;
} else {
node.text = scriptTag.text;
node.type = scriptTag.type;
//had eval here before (instead of attaching the embedded script to the HEAD).
}
document.head.appendChild(node);
scriptTag.parentNode.remove(scriptTag);
}
[/javascript]

Which partially solved the problem as both the referenced script and the embedded script was executed. However, the was not executed in the correct order. After digging around a bit I found a really great article explaining in depth how scripts are loaded into the browser (only referenced scripts).

In essence you can’t be sure of execution order per default when you include scripts dynamically (attach stuff to the DOM). To be sure of the order you need to do one of the following:

a. Use async=false if supported
b. Use readyState (for ie<10)
c. Use defer attribute.

Try to use the mentioned features in that order to be sure.

However, even if you do all that you will still get screwed if you mix embedded scripts (code in in the script tag) with referenced scripts (using src attribute). The problem is that the embedded scripts will run directly, even if the reference script tags are added before. To solve that you need to push the embedded scripts into a queue and hook the load event for all referenced scripts.

Once all referenced scripts have toggled the load event, you are free to invoke the embedded scripts (either by added the script tags to an HTML element or by eval() their text property).

Thus, each time a new view is loaded, I select all script tags:

var scripts = viewContainer.getElementsByTagName('script');
var loader = new ScriptLoader();
for (var i = 0; i < scripts.length; i++) {
    loader.loadTags(scripts[i]);
}

Which will go through all tags, force the reference script tags to load, wait for all to complete and then executed the embedded scripts.

The full typescript:

export class ScriptLoader {
	private pendingScripts: HTMLScriptElement[] = [];
	private embeddedScripts: HTMLScriptElement[] = [];
	private container: HTMLElement;
	static dummyScriptNode: any;


	constructor(container: HTMLElement = document.head) {
		this.container = container;
		if (!ScriptLoader.dummyScriptNode)
			ScriptLoader.dummyScriptNode = document.createElement("script");
	}
	private stateChange() {
		if (this.pendingScripts.length === 0) {
			if (console && console.log)
				console.log("Got ready state for a non existent script: ", this);
			return;
		}

		var firstScript = <any>this.pendingScripts[0];
		while (firstScript && firstScript.readyState === 'loaded') {
			firstScript.onreadystatechange = null;
			this.container.appendChild(firstScript);

			this.pendingScripts.shift();
			firstScript = <any>this.pendingScripts[0];
		}
		if (this.pendingScripts.length === 0) {
			this.runEmbeddedScripts();
		}
	}

	public loadSources(scripts: string|string[]) {
		if (scripts instanceof Array) {
			for (var i = 0; i < scripts.length; i++) {
				this.loadSource(scripts[i]);
			}
		} else {
			this.loadSource(<string>scripts);
		}
	}

	public loadTags(scripts: HTMLScriptElement|HTMLScriptElement[]) {
		if (scripts instanceof Array) {
			for (var i = 0; i < scripts.length; i++) {
				this.loadElement(scripts[i]);
			}
		} else {
			this.loadElement(<HTMLScriptElement>scripts);
		}
	}



	private loadSource(source: string) {
		if ('async' in ScriptLoader.dummyScriptNode) {
			let script = document.createElement('script');
			script.async = false;
			this.pendingScripts.push(script);
			script.addEventListener('load', e => this.onScriptLoaded(script));
			script.src = source;
			this.container.appendChild(script);
		}
		else if (ScriptLoader.dummyScriptNode.readyState) { // IE<10
			let script = <any>document.createElement('script');
			this.pendingScripts.push(script);
			script.onreadystatechange = this.stateChange;
			script.src = source;
		}
		else {
			let script = document.createElement('script');
			script.defer = true;
			this.pendingScripts.push(script);
			script.addEventListener('load', e => this.onScriptLoaded(script));
			script.src = source;
			this.container.appendChild(script);
		}
	}

	private loadElement(tag: HTMLScriptElement) {
		if (tag.src) {
			this.loadSource(tag.src);
			return;
		}

		let script = document.createElement('script');
		script.text = tag.text;
		this.embeddedScripts.push(script);
	}

	onScriptLoaded(script: HTMLScriptElement) {
		this.pendingScripts.pop();
		if (this.pendingScripts.length === 0) {
			this.runEmbeddedScripts();
		}

	}

	runEmbeddedScripts() {
		for (var i = 0; i < this.embeddedScripts.length; i++) {
			this.container.appendChild(this.embeddedScripts[i]);
		}
		while (this.embeddedScripts.length>0) {
			this.embeddedScripts.pop();
		}
	}
}