Waiting for dynamically loaded script
Asked Answered
D

10

83

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.

Debatable answered 5/9, 2011 at 13:38 Comment(1)
Check this related answer out: Load ordering of dynamically added script tagsDigress
F
51

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);
Footage answered 5/9, 2011 at 13:46 Comment(2)
Is there an advantage to using insertBefore instead of appendChild?Bullfight
Modern browsers only execute scripts asyncrounously or deferred if async and defer has been specified. async=false and defer=false is completely unnecessary in HTML. However, scripts that are dynamically created have async set to true by default, which is why you need the scr.async = false; in the second code sample. See html5rocks.com/en/tutorials/speed/script-loadingSwoon
F
115

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>
Freestone answered 28/3, 2017 at 17:43 Comment(3)
Note to others: the async attribute seems to be required for this to work (at least for me with a fairly small import and using Safari)Echoism
It should work with either defer or async specified. If you don't have either of those specified, the script is loaded in a blocking manner, and so I guess it will never fire the 'load' event (or it will fire it right away, before you're able to attach an event listener to it).Papoose
If the script is simple enough then I think something like <script async onload="hljs.initHighlightingOnLoad();" src="..."></script> would be more reliable because with the async keyword its possible the script loads before you're able to attach the listener. defer should be more reliable but I think the onload tag is clearest.Papoose
F
51

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);
Footage answered 5/9, 2011 at 13:46 Comment(2)
Is there an advantage to using insertBefore instead of appendChild?Bullfight
Modern browsers only execute scripts asyncrounously or deferred if async and defer has been specified. async=false and defer=false is completely unnecessary in HTML. However, scripts that are dynamically created have async set to true by default, which is why you need the scr.async = false; in the second code sample. See html5rocks.com/en/tutorials/speed/script-loadingSwoon
H
31
const jsScript = document.createElement('script')
jsScript.src =
  'https://coolJavascript.js'

jsScript.addEventListener('load', () => {
  doSomethingNow()
})

document.body.appendChild(jsScript)

Will load after the script is dynamically added

Heatstroke answered 25/11, 2019 at 23:3 Comment(4)
Its not the best idea to append the element before set the 'load' listener because if it loads too fast (cache), 'load' event will be dispatched before attaching the listener.Delagarza
@MarcosFernandezRamos - do you mean the script should be appended after the 'load' listener?Chemosynthesis
@Cansu, exactlyLysol
@MarcosFernandezRamos Event listeners run asynchronously. It won't run until the JS returns to the event loop.Loftus
S
19

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.

Sporogenesis answered 30/7, 2019 at 8:37 Comment(2)
It's not working for me, unfortunately. I'm getting the error load_scripts is not definedHatti
Working perfectly now. Thank you!Hatti
F
11

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

Flux answered 5/9, 2011 at 20:50 Comment(0)
H
5

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);
    });
}
Hairbreadth answered 1/4, 2022 at 7:5 Comment(2)
Shouldn't you add the listener before appending the element to the DOM? I know that the loading will only start after you return from the function (due to the thread-safe nature of JS) but I feel more comfortable using only fully initialized objects.Debatable
For this to work in header? change document.body.appendChild(script); to document.head.appendChild(script);Sharpshooter
L
3
    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 });
Leapfrog answered 3/9, 2021 at 9:52 Comment(0)
F
2

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 ;)

Forefather answered 10/8, 2021 at 14:37 Comment(0)
T
1

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.

  1. If the script is already loaded, it returns a Promise that resolves immediately.
  2. If the script is being loaded, it returns a Promise with interval observing that resolves if the script is loaded.
  3. 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);
};
Terr answered 13/1, 2023 at 21:12 Comment(0)
D
-3

$.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.

Denounce answered 5/9, 2011 at 13:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.