I am trying to write a HTML page that asks users a series of questions. The answers to these questions are evaluated by my JavaScript code and used to determine which additional JavaScript file the user needs to access. My code then adds the additional JavaScript file to the head tag of my HTML page. I don't want to merge the code into a single JavaScript file because these additional files are large enough to be a nightmare if they're together, and I don't want to add them all to the head when the page first loads because I will have too many variable conflicts. I'm reluctant to redirect to a new webpage for each dictionary because this will make a lot of redundant coding. I'm not using any libraries.
I begin with the following HTML code:
<head>
<link rel="stylesheet" type="text/css" href="main.css">
<script src="firstSheet.js" type="text/JavaScript"></script>
</head>
//Lots of HTML.
<div id="mainUserMenu">
</div>
And I have the following JavaScript function:
function thirdLevelQuestions(secondLevelAnswer) {
//Code here to calculate the variables. This part works.
activeDictionary = firstKey + secondKey + '.js';
//Changing the HTML header to load the correct dictionary.
document.head.innerHTML = '<link rel="stylesheet" type="text/css" href="main.css"><script src="' + activeDictionary + '" type="text/JavaScript"></script><script src="firstSheet.js" type="text/JavaScript"></script>';
//for loop to generate the next level of buttons.
for (var i = 0; i < availableOptions.length; i++) {
document.getElementById('mainUserMenu').innerHTML += '<button onclick="fourthLevelQuestions(' + i + ')">' + availableOptions[i] + '</button>';
}
}
This creates the buttons that I want, and when I inspect the head element I can see both JavaScript files there. When I click on any of the buttons at this level they should call a function in the second file. Instead Chrome tells me "Uncaught ReferenceError: fourthLevelQuestions is not defined" (html:1). If I paste the code back into firstSheet.js the function works, so I assume the problem is that my HTML document is not actually accessing the activeDictionary file. Is there a way to do this?
What Can be done
You are trying to load Javascript on Demand. This has been a well thought out problem lately and most of the native solutions didn't work well across bowser implementations. Check a study here with different solutions and background of the problem explained well.
For the case of large web applications the solution was to use some javascript library that helped with modularising code and loading them on demand using some script loaders. The focus is on modularizing code and not in just script loading. Check some libraries here. There are heavier ones which includes architectures like MVC with them.
If you use AJAX implementation of jQuery with the correct dataType jQuery will help you evaluate the scripts, they are famous for handling browser differences. You can as well take a look at the exclusive getScript() which is indeed a shorthand for AJAX with dataType script. Keep in mind that loading script with native AJAX does not guarantee evaluation of the javascript included, jQuery is doing the evaluation internally during the processing stage.
What is wrong
What you have done above might work in most modern browsers (not sure), but there is an essential flaw in your code. You are adding the script tags to your head using innerHTML which inserts those lines to your HTML. Even if your browser loads the script it takes a network delay time and we call it asynchronous loading, you cannot use the script right away. Then what do you do? Use the script when its ready or loaded. Surprisingly you have an event for that, just use it. Can be something like:
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.onreadystatechange= function () {
if (this.readyState == 'complete') helper();
}
script.onload= helper;
script.src= 'helper.js';
head.appendChild(script);
Check this article for help with implementation without using external libraries
From the variable name activeDictionary If I guess that you are loading some data sets as opposed to javascript programs, you should try looking into JSON and loading and using them dynamically.
If this Question/Answer satisfies your needs, you should delete your question to avoid duplicate entries in SO.
The best way to achieve this would be with jQuery:
$(document).ready(function() {
$('#button').click(function() {
var html = "<script src='newfile.js' type='text/javascript'></script>";
var oldhtml = "<script src='firstSheet.js' type='text/javascript'></script>";
if ($(this).attr('src') == 'firstSheet.js') {
$('script[src="firstSheet.js"]').replace(html);
return;
}
$('script[src="newfile.js"]').replace(oldhtml);
});
});
I would suggest you create the elements how they should be and then append them. Also, if you are dynamically adding the firstSheet.js you shouldn't include it in your .html file.
function thirdLevelQuestions(secondLevelAnswer) {
var mainUserMenu = document.getElementById('mainUserMenu');
activeDictionary = firstKey + secondKey + '.js';
var css = document.createElement('link');
css.rel = 'stylesheet';
css.type = 'text/css';
css.href = 'main.css';
var script1 = document.createElement('script');
script1.type = 'text/javascript';
script1.src = 'firstSheet.js';
var script2 = document.createElement('script');
script2.type = 'text/javascript';
script2.src = activeDictionary;
document.head.appendChild(css);
document.head.appendChild(script1);
document.head.appendChild(script2);
for (var i = 0; i < availableOptions.length; i++) {
var btn = document.createElement('button');
btn.onclick = 'fourthLevelQuestions(' + i + ')';
var val = document.createTextNode(availableOptions[i]);
btn.appendChild(val);
mainUserMenu.appendChild(btn);
}
}
I'm very new to javascript (c++ normally) and I think this question should be quite basic for you all.
I have a script that gets a variable defined in an external .js file and displays it using an alert.
The code in the .html file looks like this.
<html>
<head>
<title></title>
<script>
function addScript(url){
var extScript = document.createElement('script');
extScript.type = 'text/javascript';
extScript.src = url;
extScript.id = 'extScript'
//If there is already a script with the ID 'extScript'
//get rid of it
var headList = document.getElementsByTagName('head');
var scriptList = headList[0].getElementsByTagName('script');
for(var i = 0; i < scriptList.length; i++)
{
if(scriptList[i].id =='extScript')
{
document.getElementsByTagName('head')[0].removeChild(scriptList[i]);
}
}
document.getElementsByTagName('head')[0].appendChild(extScript);
}
function newNewChangeMode()
{
addScript("C:/Users/Suzaku/Documents/Javascript/controller.js");
alert("Neo controllerMode variable is reading " + controllerMode);
}
</script>
</head>
<body>
Get externally defined mode
</body>
</html>
And the file "controller.js" looks like this.
var controllerMode = 1111;
(that's it!)
When I click the link "get externally defined mode", my javascript runs and the alert is displayed correctly. Displaying "Neo controllerMode variable is reading 1111".
However, if I change the variable controllerMode's definition (in controller.js) to
var controllerMode = 2222;
,hit save, and click the the button again (without refreshing), it still alerts "Neo controllerMode variable is reading 1111". Whereas it SHOULD say "Neo controllerMode variable is reading 2222".
It would seem that this script is not being added dynamically. I need to be able to change this variable without having to refresh the .html.
Thanks in advance,
Guy
It sounds like your browser is only retrieving the cached javascript file. Make sure you go directly to your updated javascript file (controller.js) and hit F5 to ENSURE that you are loading a new version, not the cached one. Otherwise your browser will keep plugging the old, cached script (which has 1111 defined) into your script. Common problem!
Problem
In order to improve the page performance I need to preload scripts that I will need to run on the bottom page.
I would like to take control of when the script is parsed, compiled and executed.
I must avoid the script tag, because it is blocker for common render engines (geeko, etc).
I can't load it using defer property, because I need to control when the script is executed.
Also, async property is not a possibility.
sample:
<html><head>
//preload scripts ie: a.js without use the script
</head><body> ..... all my nice html here
//execute here a.js
</body></html>
This allows me to maximize the render performance of my page, because the browser will start to donwload the scripts content, and it will render the page at the same time in parallel. Finally, I can add the script tag, so the browser will parse, compile and execute the code.
The only way that I could do that is using a hidden image tag. (This is a simplified version of Stoyan)
i.e.
<html><head>
<img src="a.js" style=display:none;>
</head><body> ..... all my nice html here
<script src="a.js">
</body></html>
Question
I didn't find any problem using this technique, but does anyone know a better way to do this?
Is there any meta prefetch?
Additional information
I'm using requirejs, so I'm trying to preload the modules code, without executing it, because this code depends of DOM elements.
With similar technique you may preload scripts and stylesheets using img for Internet Explorer and object tag for every other browser.
var isMSIE = /*#cc_on!#*/false;
var resources = ['a.js', 'b.js', 'c.css'];
for (var i=0; i<resources.length; i++){
if (isMSIE){
new Image().src = resources[i];
} else {
var o = document.createElement('object');
o.data = resources[i];
document.body.appendChild(o);
}
}
There is a blog post describing such a technique and outlining caveats: Preload CSS/JavaScript without execution.
But why don't you want to just use dynamically added scripts just like suggested in other answer, this will probably lead to a cleaner solution with more control.
You can use the prefetch attribute of a link tag to preload any resource, javascript included. As of this writing (Aug 10, 2016) it isn't supported in Safari, but is pretty much everywhere else:
<link rel="prefetch" href="(url)">
More info on support here:
http://caniuse.com/#search=prefetch
Note that IE 9,10 aren't listed in the caniuse matrix because Microsoft has discontinued support for them.
More info here and more options for preloading, like prerender and more
For each script you'd like to download without executing, make an object containing a name and a url, and put those objects into an array.
Looping through the array, use jQuery.ajax with dataType: "text" to download your scripts as text. In the done handler of the ajax call, store the text content of the file (which is passed in first argument) in the appropriate object, increment a counter, and call an "alldone" function when that counter is equal to the number of files you are downloading in this manner.
In the "alldone" function (or later) do the following: Loop through your array again, and for each entry, use document.createElement("script"), document.createTextNode(...), and (...scriptNode...).appendChild(...) to dynamically generate scripts having the intended source inline, rather than via "src" attribute. Finally, do document.head.appendChild(...scriptNode...), which is the point when that script is executed.
I have used this technique in a project where I needed to use frames, where several frames and/or the frameset need identical JavaScript files, in order to make sure each of those files is requested only once from the server.
Code (tested and working) follows
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<html>
<head>
<script id="scriptData">
var scriptData = [
{ name: "foo" , url: "path/to/foo" },
{ name: "bar" , url: "path/to/bar" }
];
</script>
<script id="scriptLoader">
var LOADER = {
loadedCount: 0,
toBeLoadedCount: 0,
load_jQuery: function (){
var jqNode = document.createElement("script");
jqNode.setAttribute("src", "/path/to/jquery");
jqNode.setAttribute("onload", "LOADER.loadScripts();");
jqNode.setAttribute("id", "jquery");
document.head.appendChild(jqNode);
},
loadScripts: function (){
var scriptDataLookup = this.scriptDataLookup = {};
var scriptNodes = this.scriptNodes = {};
var scriptNodesArr = this.scriptNodesArr = [];
for (var j=0; j<scriptData.length; j++){
var theEntry = scriptData[j];
scriptDataLookup[theEntry.name] = theEntry;
}
//console.log(JSON.stringify(scriptDataLookup, null, 4));
for (var i=0; i<scriptData.length; i++){
var entry = scriptData[i];
var name = entry.name;
var theURL = entry.url;
this.toBeLoadedCount++;
var node = document.createElement("script");
node.setAttribute("id", name);
scriptNodes[name] = node;
scriptNodesArr.push(node);
jQuery.ajax({
method : "GET",
url : theURL,
dataType : "text"
}).done(this.makeHandler(name, node)).fail(this.makeFailHandler(name, node));
}
},
makeFailHandler: function(name, node){
var THIS = this;
return function(xhr, errorName, errorMessage){
console.log(name, "FAIL");
console.log(xhr);
console.log(errorName);
console.log(errorMessage);
debugger;
}
},
makeHandler: function(name, node){
var THIS = this;
return function (fileContents, status, xhr){
THIS.loadedCount++;
//console.log("loaded", name, "content length", fileContents.length, "status", status);
//console.log("loaded:", THIS.loadedCount, "/", THIS.toBeLoadedCount);
THIS.scriptDataLookup[name].fileContents = fileContents;
if (THIS.loadedCount >= THIS.toBeLoadedCount){
THIS.allScriptsLoaded();
}
}
},
allScriptsLoaded: function(){
for (var i=0; i<this.scriptNodesArr.length; i++){
var scriptNode = this.scriptNodesArr[i];
var name = scriptNode.id;
var data = this.scriptDataLookup[name];
var fileContents = data.fileContents;
var textNode = document.createTextNode(fileContents);
scriptNode.appendChild(textNode);
document.head.appendChild(scriptNode); // execution is here
//console.log(scriptNode);
}
// call code to make the frames here
}
};
</script>
</head>
<frameset rows="200pixels,*" onload="LOADER.load_jQuery();">
<frame src="about:blank"></frame>
<frame src="about:blank"></frame>
</frameset>
</html>
other question closely related to above approach
other related question
You should have a look at the following links:
http://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/
http://tomdale.net/2012/01/amd-is-not-the-answer/
And at how ember.js is using a tool called minispade and preprocessing with ruby to make the process of loading, parsing and running javascript modules fast.
Why not to try this?
var script = document.createElement('script');
script.src = 'http://path/to/your/script.js';
script.onload = function() {
// do something here
}
document.head.appendChild(script);
you can use .onload event to control when it is loaded. One caveat is that .onload() doesn't work in IE and you can use this:
script.onreadystatechange = function() {
if (/^loaded|complete$/i.test(this.readyState)) {
// loaded
};
}
Additionally adding scripts via DOM is non-blocking and i believe you can perfectly achieve your goals with this approach.
I've answered the same question there:
https://stackoverflow.com/a/46121439/1951947
just use the <link> tag to preload your script and then you can use it with the <script> tag
eg: <link href="/js/script-to-preload.js" rel="preload" as="script">
I'm using jquery to rewrite a list of links on the page. If the location.host is NOT the vendor location.host AND the cookie isn't set to a specific value then it locates the links and rewrites them to the alternate values. The code I'm using works great in FF but not in IE7. Please help!
<script type="text/javascript">
// link hider
var hostadd = location.host;
var vendor = '172.29.132.34';
var localaccess = 'internal.na.internal.com';
var unlock = 'http://internal.na.internal.com/Learning/Customer_Care/navigation/newhire.html';
// link rewriter
$(document).ready (
function style_switcher(){
//if not a vendor or not accessing from lms reroute user to lms
if (hostadd != vendor && $.cookie("unlockCookie") != unlock){
var linkData = {
"https://www.somesite.com": "https://internalsite.com/something",'../Compliance/something/index.html':'../somethingelse.html'
};
$("a").each(function() {
var link = this.getAttribute("href"); // use getAttribute to get what was actualy in the page, perhaps not fully qualified
if (linkData[link]) {
this.href = linkData[link];
}
});
}
});
</script>
What you could do, if you insert the links dynamic, is store them in a data attribute like data-orglink="yourlink" which wouldnt be transformed by the browser, then check on that -and if its in the object array - change the href. Do you have access to creating the data attribute?
IE7 have problems with internal links, because it puts the host info on, before JS can reach the link..
http://jsfiddle.net/Cvj8C/9/
Will work in all, but IE7. So you need to use full paths if to use JS for this function :(
You had some errors in your JS.
But it seems to work fine?
See: http://jsfiddle.net/s4XmP/
or am i missing something? :)
I am loading scripts and style-sheets dynamically from javascript like this.
The problem is that browser does not wait for the script to load.
consider i have a function named functionToBeCalled() inside a script file named script-file.js
i have a function to load script file.
<script type="text/javascript">
var listOfJavaScriptsLoaded = new Array();
function LoadScriptFile(scriptUrl){
var isScriptLoaded = false;
var i = 0;
for(i = 0; i < listOfJavaScriptsLoaded.length; i ++){
if(listOfJavaScriptsLoaded[i] == scriptUrl){
isScriptLoaded = true;
break;
}
}
if(isScriptLoaded == false){
var headTag= document.getElementsByTagName('head')[0];
var scriptTag= document.createElement('script');
scriptTag.type= 'text/javascript';
scriptTag.src= scriptUrl;
headTag.appendChild(scriptTag);
listOfJavaScriptsLoaded.push(scriptUrl);
}
}
LoadScriptFile("script-file.js");
functionToBeCalled();
</script>
now, what happens is that the browser does not wait for the script tag to load and goes to the next command. I get a "undefined functionToBeCalled()" error. this is natural. But the fact is that when i inspect in firebug, the script tag has been formed and the file has loaded.
So how do i make the browser to pause loading and resume after the asset has been loaded?
Edit1: This problem occurs only when i am loading the page in ajax and not in normal page loads
Edit2: Or is there a possibility to read a script/css file from javascript and write it directly in the markup within script tags
If i use window.stop() the loading stops completely. how can i make it resume from the same line?
Or is it possible to make the browser to consider that the loading is still happening and reset it in the onload event?
You may have specific reasons to load the script dynamically, but to present the option, if you write out the script element in your HTML output like so:
<script src="script-file.js"></script>
<script>functionToBeCalled();</script>
the browser will halt parsing until that script has been loaded, and interpreted.
This is also valid in the BODY.
Check out LABjs ( http://labjs.com/ ) by Getify Solutions. LABjs allows script-inserted scripts to be loaded concurrently but run in order.
pretty much every tag which loads a resource has an onload event. so in plain javascript this means in your case something like this:
var s = document.createElement('script');
s.src = "script-file.js";
s.type = 'text/javascript';
head.appendChild(s);
s.onload = function(){
functionToBeCalled();
}
I would recommend looking at Cuzillion. It will allow you to experiment with calling javascript and css in many different ways to see how they react in the browser.
This should answer your question. Just execute it before your page is done loading the body.
<script type="text/javascript">
var loadScriptFile = (function(){
var listOfJavaScriptsLoaded = [];
return function(scriptUrl){
var isScriptLoaded = false;
for(var i = 0; i < listOfJavaScriptsLoaded.length; i ++){
if(listOfJavaScriptsLoaded[i] == scriptUrl){
isScriptLoaded = true;
break;
}
}
if(!isScriptLoaded){
document.write('<scr' + 'ipt type="text/javascript" src="' + scriptUrl + '"></scr' + 'ipt>');
}
};
}());
loadScriptFile("script-file.js");
functionToBeCalled();
</script>