today I've been working on loading dynamic javascript code (files). The solution I use is :
function loadScript(scriptUrl) {
var head = document.getElementsByTagName("head")[0];
var script = document.createElement('script');
script.id = 'uploadScript';
script.type = 'text/javascript';
script.src = scriptUrl;
head.appendChild(script);
}
the problem with this is that I don't know how to make sure WHEN the script contents are executed. Since the script contains classes (well JS classes), I wrote down a function that is called through setTimeout and checks if those objects are defined. This is not flexible as it is not automatical. So? Are there ways to load those scripts and have a reliable notification on when they have been executed?
You can use jquery's getScript and pass a callback function.
$.getScript("test.js", function(){
alert("Script loaded and executed.");
});
See: jquery.
The easiest way short of a JS library is to make an XMLHttpRequest and eval() the return. Your "onload" would be when the data is returned, and "oninit" would be right after you've eval'd.
EDIT: If you want to sequence this, you can create an AssetLoader that takes an array of scripts and won't eval() a script until the one preceding it has been fetched and initialized.
EDIT 2: You can also use the script.onload stuff in the post referenced in the comments. The eval method has a slight advantage in that you can separate and control the load and execution portions of the script import.
EDIT 3: Here's an example. Consider a file called foo.js that contains the following:
function foo () {
alert('bar');
}
Then execute the following from another page in your browser:
function initScript (scriptString) {
window.eval(scriptString);
}
function getScript (url, loadCallback, initCallback, callbackScope) {
var req = new XMLHttpRequest();
req.open('GET', url);
req.onreadystatechange = function (e) {
if (req.readyState == 4) {
if (loadCallback) loadCallback.apply(callbackScope);
initScript.call(null, req.responseText);
if (initCallback) initCallback.apply(callbackScope);
}
}
req.send();
}
function fooScriptLoaded () {
alert('script loaded');
}
function fooScriptInitialized () {
alert('script initialized');
foo();
}
window.onload = function () {
getScript('foo.js', fooScriptLoaded, fooScriptInitialized, null);
}
You will see the alerts "script loaded", "script initialized", and "bar". Obviously the implementation of XMLHttpRequest here isn't complete and there are all sorts of things you can do for whatever scope you want to execute the script in, but this is the core of it.
You could use a counter variable, and a kind of callback:
var scriptsToLoad = 10;
var loadedScripts = 0;
//...
function increaseLoadCount() {
loadedScripts++;
if(loadedScripts == scriptsToLoad) {
alert("start here");
}
}
//script1-10.js
increaseLoadCount();
Maybe a bit nicer than with a timeout..
Related
In my page body, I need to insert this code as the result of an AJAX call:
<p>Loading jQuery</p>
<script type='text/javascript' src='scripts/jquery/core/jquery-1.4.4.js'></script>
<p>Using jQuery</p>
<script type='text/javascript'>
$.ajax({
...
});
</script>
I can't use $.load() since the document has already loaded, so the event doesn't fire.
Is this safe? If not, how do I make sure the jquery script has loaded before my custom, generated code is executed.
Add an ID to your script file so you can query it.
<script id="hljs" async src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.0.0/highlight.min.js"></script>
Then add a load listener to it in JavaScript
<script>
var script = document.querySelector('#hljs');
script.addEventListener('load', function() {
hljs.initHighlightingOnLoad();
});
</script>
It is pretty safe. Historically, <script> tags are full blocking, hence the second <script> tag can't get encountered befored the former has finished parsing/excuting. Only problem might be that "modern" browsers tend to load scripts asynchronously and deferred. So to make sure order is correct, use it like this:
<p>Loading jQuery</p>
<script type='text/javascript' async=false defer=false src='scripts/jquery/core/jquery-1.4.4.js'></script>
<p>Using jQuery</p>
<script type='text/javascript'>
$.ajax({
...
});
</script>
However, it's probably a better idea it use dynamic script tag insertion instead of pushing this as HTML string into the DOM. Would be the same story
var scr = document.createElement('script'),
head = document.head || document.getElementsByTagName('head')[0];
scr.src = 'scripts/jquery/core/jquery-1.4.4.js';
scr.async = false; // optionally
head.insertBefore(scr, head.firstChild);
const jsScript = document.createElement('script')
jsScript.src =
'https://coolJavascript.js'
document.body.appendChild(jsScript)
jsScript.addEventListener('load', () => {
doSomethingNow()
})
Will load after the script is dynamically added
Wait for multiple scripts to load
The following helper loads multiple scripts only once and returns a promise:
async function cirosantilli_load_scripts(script_urls) {
function load(script_url) {
return new Promise(function(resolve, reject) {
if (cirosantilli_load_scripts.loaded.has(script_url)) {
resolve();
} else {
var script = document.createElement('script');
script.onload = resolve;
script.src = script_url
document.head.appendChild(script);
}
});
}
var promises = [];
for (const script_url of script_urls) {
promises.push(load(script_url));
}
await Promise.all(promises);
for (const script_url of script_urls) {
cirosantilli_load_scripts.loaded.add(script_url);
}
}
cirosantilli_load_scripts.loaded = new Set();
(async () => {
await cirosantilli_load_scripts([
'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js',
]);
// Now do stuff with those scripts.
})();
GitHub upstream: definition and usage.
Tested in Chromium 75.
There is also new feature in jQuery 1.6. It is called jQuery.holdReady(). It is actually self explanatory; when you call jQuery.holdReady(true), ready event is not fired until you call jQuery.holdReady(false). Setting this to false will not automatically fire a ready event, it just removes the hold.
Here is a non-blocking example of loading a script taken from the documentation:
$.holdReady(true);
$.getScript("myplugin.js", function() {
$.holdReady(false);
});
See http://api.jquery.com/jQuery.holdReady/ for more information
this works for me
function loadScript(sources, callBack) {
var script = document.createElement('script');
script.src = sources;
script.async = false;
document.body.appendChild(script);
script.addEventListener('load', () => {
if(typeof callBack == "function") callBack(sources);
});
}
In my case the solutions didn't work. I wanted to programmatically click a link after a script has loaded the right click event for the link. Thus I had to work with timeout and loop:
<script>
var ifrbutton = document.getElementById("id-of-link");
if(typeof(ifrbutton) != 'undefined' && ifrbutton != null && window.location.hash == "#special-hash") {
var i = 0;
// store the interval id to clear in future
var intr = setInterval(function() {
if (ifrbutton.onclick!=null) {
ifrbutton.click();
clearInterval(intr);
i = 200;
}
if (++i >= 200) clearInterval(intr);
}, 300)
}
</script>
Thus the solution could also be to check in intervals for certain functionality that the script brings with it... Set some secure end just in case the script gets never loaded... Works safely for me ;)
new MutationObserver((mutationsList, observer) => {
for (var mutation of mutationsList)
if (mutation.type === 'childList')
Array.from (mutation.addedNodes)
.filter (node => node.tagName === 'SCRIPT')
.forEach (script => {
//Script started loading
script.addEventListener ('load', () => {
//Script finished loading
})
})
}).observe(document, { attributes: false, childList: true, subtree: true });
The answer from Ciro Santilli is excellent. I have improved it a little bit because I faced some issues.
I made a special form field based on CodeMirror editor. Since the script for the CodeMirror is huge, I load it only on demand by field initialization.
If the form has 2 such fields, the loading of the script is started twice merely at the same moment, since the first started script is not loaded yet, it is actually loaded twice, what is bad.
The functions MyNamespace.loadScript(script_url) and MyNamespace.loadScripts(script_urls) always return a Promise so that you can decide whether to use await or then.
If the script is already loaded, it returns a Promise that resolves immediately.
If the script is being loaded, it returns a Promise with interval observing that resolves if the script is loaded.
If the script was never loaded, it returns a Promise with loading script that resolves by loaded event.
The code:
MyNamespace.loadScript = function (script_url) {
console.log("loading: " + script_url);
if (!MyNamespace.loadedScripts) MyNamespace.loadedScripts = new Set();
if (!MyNamespace.loadingScripts) MyNamespace.loadingScripts = new Set();
if (MyNamespace.loadedScripts.has(script_url)) {
return new Promise(function (resolve, reject) {
console.log("already loaded");
resolve();
});
}
if (MyNamespace.loadingScripts.has(script_url)) {
return new Promise(function (resolve, reject) {
console.log("loading in progress");
let interval = setInterval(function () {
if (MyNamespace.loadedScripts.has(script_url)) {
clearInterval(interval);
console.log("waited until loaded");
resolve();
}
}, 200);
});
}
MyNamespace.loadingScripts.add(script_url);
return new Promise(function (resolve, reject) {
let script = document.createElement('script');
script.src = script_url;
script.onload = function () {
console.log("actually loaded");
MyNamespace.loadedScripts.add(script_url);
resolve();
};
script.onerror = function () {
console.log("load error");
reject(new Error("Error by loading the script " + script_url));
};
document.head.append(script);
});
};
MyNamespace.loadScripts = function (script_urls) {
let promises = [];
for (let script_url of script_urls) {
promises.push(MyNamespace.loadScript(script_url));
}
return Promise.all(promises);
};
$.ajax and $.load are the same thing. You can use either. If you put $.load in a script tag on the page it will fire when it loads just like $.ajax().
When it comes to waiting for a script to fire before you do something what Tomalak said is kinda true. The exception is asynchronous calls. Javascript will make an AJAX call then continue running the script and when the AJAX call responds it will run the success/fail depending on what you tell it to do.
Now say you want your JS to wait for the return, with a single call it's pretty easy just wrap every thing in the success callback, or you could do it the cool way and use $.deferred. This allows you to make a bunch of ajax calls or one and then run some code only after the finish.
$.when & $.then are probably the best for this situation.
Any way what your are doing is safe.
You might be familiar with the good old Jquery load fallback:
<script>window.jQuery || document.write('<script src="https://example.com/jquery.js"></script>')</script>
But I read here and there: don’t use document.write, is bad for your health, it does not work on Chrome (It’s working for me, Chrome 78).
So I’m trying to replace it, but I’m not able to find a solution that will load synchronously the new js file, before DOM loaded is triggered.
And what ends happening with a DOM manipulation alternative is that the browser consideres the DOM is loaded and all $(document).ready() fail with “$ is not defined”.
function Jqfallback() {
var j = document.createElement('script');
j.src = 'https://example.com/jquery.js';
document.getElementsByTagName('head')[0].appendChild(j);
}
(window.jQuery || Jqfallback() );
No matter where I put this script, or the new JS file, which in this case ('head')[0] is already before all other JS which are in the body, it loads it “asyncronically”.
Is there another option or I continue rocking document.write() in late 2019?
It takes a bit of time to load and parse JQuery. So use a (small) timeout after appending the script.
This snippet wraps conditional loading in a immediately executed anonymous function:
(myScripting => {
if (!window.$) {
let j = document.createElement('script');
j.src = '//code.jquery.com/jquery-3.4.1.slim.min.js';
document.querySelector('head').appendChild(j);
setTimeout( myScripting, 200 );
} else {
myScripting();
}
})(JqIsLoadedSoMyScriptingCanStart);
// put your main scripting in here
function JqIsLoadedSoMyScriptingCanStart() {
// extra check
if (!window.$) {
alert("Sorry, JQuery is not loaded, can't continue");
return;
}
console.log("JQuery in place?");
console.log($("head script")[1]);
}
<script src="cantLoadThis"></script>
Place the code that uses jQuery in the onload() function.
var jQuery1 = document.createElement('script');
jQuery1.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js";
jQuery1.onload = function () {
var $ = window.jQuery;
$.when(
$.getScript("https://someOtherScript.js"), //if you need
$.Deferred(function (deferred) {
$(deferred.resolve);
})
).done(function () {
console.log("all scripts loaded!!");
doNextTask(); //some other code which uses jQuery
});
};
Append jQuery to your document in onreadystatechange
document.onreadystatechange = function () {
if (document.readyState == "complete") {
// document is ready.
document.head.appendChild(jQuery1);
}
}
I have a array where i have specified the files i need to load in javascript before calling specific script. Lets call those particular lines of code as myscript.
I did as follows
var fileNamesArray = new Array();
fileNamesArray.push("abc.js");
fileNamesArray.push("pqr.js");
fileNamesArray.push("xyz.js");
fileNamesArray.push("klm.js");
var totalFiles = jQuery(fileNamesArray).length;
var tempCount = 0;
jQuery.each(fileNamesArray, function(key, value) {
jQuery.getScript(value, function() {
tempCount++;
});
});
to check whether all files are being loaded or not, i done following thing but doesn't seems to be effective
var refreshIntervalId = setInterval(function() {
if (tempCount == totalFiles) {
clearInterval(refreshIntervalId);
return;
}
}, 10);
i have implemented these in object oriented javascript as follows
function Loader() {
this.jQuery = null;
// check for specifically jQuery 1.8.2+, if not, load it
if (jQuery == undefined) {
jQuery.getScript(
"/Common/javascript/jquery/map/javascript/jquery-1.8.2.js",
function() {
this.jQuery = jQuery.noConflict();
});
} else {
var jQueryVersion = $.fn.jquery;
jQueryVersion = parseInt(jQueryVersion.split('.').join(""));
if (182 > jQueryVersion) {
jQuery.getScript(
"/Common/javascript/jquery/map/javascript/jquery-1.8.2.js",
function() {
this.jQuery = jQuery.noConflict();
});
}
}
}
Loader.prototype.LoadAllFile = function() {
//here i am loading all files
}
Loader.prototype.bindMap = function(options) {
this.LoadAllFile();
//execute the script after loading the files... which we called as myscript
}
i am loading more than 12-14 js files via ajax.
if you observe Loader.prototype.bindMap, i am loading all the files first and then executing the script.
But it seems that myscript the script start executing before all files being loaded.
what are the better ways to execute the script only after all js files are loaded.
Take a look at jQuery's .load() http://api.jquery.com/load-event/
$('script').load(function () { });
Based on the documentation on Jquery.getScript , it is a shorthand for Jquery.ajax. By default this in async call. You might want to change it to do a synchronous call.
To set this property, you can refer to this
So instead of doing a setInterval, you can just loop in your array and do a Jquery.getScript.
I found a little javascript snippet for including javascripts only if they was not included before.
That is working with my own scripts, but with two third-party libraries it's not working and I really don't know why.
var included_files = new Array();
function include_once(script_filename) {
if (!in_array(script_filename, included_files)) {
included_files[included_files.length] = script_filename;
include_dom(script_filename);
}
}
function in_array(needle, haystack) {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] == needle) {
return true;
}
}
return false;
}
function include_dom(script_filename) {
var html_doc = document.getElementsByTagName('head').item(0);
var js = document.createElement('script');
js.setAttribute('language', 'javascript');
js.setAttribute('type', 'text/javascript');
js.setAttribute('src', script_filename);
html_doc.appendChild(js);
return false;
}
function loaded() {
include_once("shared/scripts/jquery.min.js");
include_once("shared/scripts/iscroll.js");
$(document).ready(function () {
alert("hello");
});
}
error: $ is not defined.
If I import jQuery the regular way its working and it says "iScroll" is not defined (because I'm using it later).
Any ideas?
include_dom is asynchronous. It loads the scripts in parallel, and you can't really determine when the scripts will be loaded. You try to use jQuery right after you started the download, which doesn't work.
You need to use a script that allows you to specify a callback for loaded scripts. I would recommend require.js
You are adding the scripts to the DOM, but not letting them load before you try to use the functions they provide.
You need to bind a callback to the load event of the script elements you are adding.
(At least in most browsers, you might have to implement some hacks in others; you may wish to examine the source code for jQuery's getScript method).
Did someone say callback?
function include_once(script_filename, callback) {
if (!in_array(script_filename, included_files)) {
included_files[included_files.length] = script_filename;
include_dom(script_filename, callback);
}
}
function include_dom(script_filename, callback) {
var html_doc = document.getElementsByTagName('head').item(0);
var js = document.createElement('script');
js.setAttribute('language', 'javascript');
js.setAttribute('type', 'text/javascript');
js.setAttribute('src', script_filename);
if(callback && callback != 'undefined'){
js.onload = callback;
js.onreadystatechange = function() {
if (this.readyState == 'complete') callback();
}
}
html_doc.appendChild(js);
return false;
}
function loaded() {
include_once("shared/scripts/jquery.min.js", function(){
$(document).ready(function () {
alert("hello");
});
});
include_once("shared/scripts/iscroll.js");
}
Use a script loader. yepnope will do everything you are trying to do and more
UPDATE:
I have the following code:
<script type="text/javascript">
function addScript(url) {
var script = document.createElement('script');
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
addScript('http://google.com/google-maps.js');
addScript('http://jquery.com/jquery.js');
...
// run code below this point once both google-maps.js & jquery.js has been downloaded and excuted
</script>
How can I prevent code from executing until all required JS have been downloaded and executed? In my example above, those required files being google-maps.js and jquery.js.
You can use the onload event of the script element for most browsers, and use a callback argument:
Edit: You can't really stop the execution of the code when you load scripts in this way (and making synchronous Ajax requests is a bad idea most of the times).
But you can chain callbacks, so if you have some code that depends on both, Two.js and Three.js, you can chain the loading actions, for example:
loadScript('http://example.com/Two.js', function () {
// Two.js is already loaded here, get Three.js...
loadScript('http://example.com/Three.js', function () {
// Both, Two.js and Three.js loaded...
// you can place dependent code here...
});
});
Implementation:
function loadScript(url, callback) {
var head = document.getElementsByTagName("head")[0],
script = document.createElement("script"),
done = false;
script.src = url;
// Attach event handlers for all browsers
script.onload = script.onreadystatechange = function(){
if ( !done && (!this.readyState || // IE stuff...
this.readyState == "loaded" || this.readyState == "complete") ) {
done = true;
callback(); // execute callback function
// Prevent memory leaks in IE
script.onload = script.onreadystatechange = null;
head.removeChild( script );
}
};
head.appendChild(script);
}
For IE, the onreadystatechange event has to be bound.
I just read CMS's answer, and decided that from his "most browsers" comment, I might have a crack at getting it work for ones that do not have this functionality natively.
Basically, it's an interval that polls for a variable.
var poll = window.setInterval(function() {
if (typeof myVar !== 'undefined') {
clearInterval(poll);
doSomething();
};
}, 100);