90

How can I inject a <script src="https://remote.com/"></script> element into my page, wait for it to execute, and then use functions that it defines?

FYI: In my case, the script will do some credit card processing in rare cases, so I don't want to include it always. I want to include it quickly when the user opens up a change-credit-card-options dialog, and then send it the new credit card options.

Edit for additional detail: I do not have access to the remote script.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Riley Lark
  • 20,660
  • 15
  • 80
  • 128
  • Do you have control over the remote script? If so it might be easier to have the script itself call your code when it is done, a-la JSONP – hugomg Dec 20 '11 at 16:44
  • Unfortunately I don't have control over the remote script. Editing question to reflect that~ – Riley Lark Dec 20 '11 at 18:18

7 Answers7

165

You could use Google Analytics or Facebook's method:

(function(d, script) {
    script = d.createElement('script');
    script.type = 'text/javascript';
    script.async = true;
    script.onload = function(){
        // remote script has loaded
    };
    script.src = 'http://www.google-analytics.com/ga.js';
    d.getElementsByTagName('head')[0].appendChild(script);
}(document));

UPDATE:

Below is the new Facebook method; it relies on an existing script tag instead of <head>:

(function(d, s, id){
    var js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)){ return; }
    js = d.createElement(s); js.id = id;
    js.onload = function(){
        // remote script has loaded
    };
    js.src = "//connect.facebook.net/en_US/sdk.js";
    fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
  • Replace facebook-jssdk with your unique script identifier to avoid it being appended more than once.
  • Replace the script's url with your own.
typeoneerror
  • 55,990
  • 32
  • 132
  • 223
Pierre
  • 18,643
  • 4
  • 41
  • 62
  • This seems perfect, but there is some chatter on the internet that onload isn't called by all browsers. Do you know what browsers this would work with? – Riley Lark Dec 20 '11 at 18:23
  • Why do you not append this to the body? – Ben Taliadoros Oct 20 '14 at 12:40
  • 1
    Instead of using `script.onload` or `js.onload`, I would add an event listener to the `load` event. http://stackoverflow.com/questions/15564029/adding-to-window-onload-event – ha404 Jun 05 '15 at 20:56
  • Is there a benefit to appending to another `script` vs appending it to the `head`? The downside I see to appending to `script`s is if there aren't any to append to it'd break. – ha404 Dec 22 '15 at 23:25
  • In my case, I needed this for use as a **bookmarklet**, in which case this bookmarklet generator service was useful: http://mrcoles.com/bookmarklet/ (just make sure you remove any comments from the code, as it does not filter those out by itself) – ohaal Feb 11 '16 at 12:30
  • 8
    Note that the first query `d.getElementsByTagName(s)[0]` can be moved below the `if` check to save yourself a DOM query if the script already exists. – mAAdhaTTah Mar 14 '16 at 16:16
  • How can it rely on the existing script tag in dom? injection of `facebook-jssdk` will raise an error if the user doesn't have any script tag in dom. – muratgozel May 18 '19 at 10:07
  • 1
    could not see any reason for using the load event in the attached question, can you please clarify ? @ha404 – gaurav5430 Jan 24 '21 at 14:02
59

Same method using event listeners and ES2015 constructs:

function injectScript(src) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = src;
        script.addEventListener('load', resolve);
        script.addEventListener('error', e => reject(e.error));
        document.head.appendChild(script);
    });
}

injectScript('https://example.com/script.js')
    .then(() => {
        console.log('Script loaded!');
    }).catch(error => {
        console.error(error);
    });
Frank Gambino
  • 729
  • 6
  • 6
16

This is one way to dynamically load and execute a list of scripts synchronously. You need to insert each script tag into the DOM, explicitly setting its async attribute to false:

script.async = false;

Scripts that have been injected into the DOM are executed asynchronously by default, so you have to set the async attribute to false manually to work around this.

Example

<script>
(function() {
  var scriptNames = [
    "https://code.jquery.com/jquery.min.js",
    "example.js"
  ];
  for (var i = 0; i < scriptNames.length; i++) {
    var script = document.createElement('script');
    script.src = scriptNames[i];
    script.async = false; // This is required for synchronous execution
    document.head.appendChild(script);
  }
  // jquery.min.js and example.js will be run in order and synchronously
})();
</script>

<!-- Gotcha: these two script tags may still be run before `jquery.min.js`
     and `example.js` -->
<script src="example2.js"></script>
<script>/* ... */<script>

References

Flimm
  • 136,138
  • 45
  • 251
  • 267
9

Dynamic import()

Using dynamic import, you can now load modules and wait for them to excute, as simply as this:

import("http://example.com/module.js").then(function(module) {
  alert("module ready");
});

If the module has already been loaded and executed, it won't get loaded and executed again, but the promise returned by import will still resolve.

Note that the file is loaded as a module, not as just a script. Modules are executed in strict mode, and they are loaded in module scope, which means variables are not automatically made global the way they are in normally loaded scripts. Use the export keyword in a module to share a variable with other modules or scripts.

References:

Flimm
  • 136,138
  • 45
  • 251
  • 267
6

something like this should do the trick:

(function() {
    // Create a new script node
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.onload = function() {
        // Cleanup onload handler
        script.onload = null;

        // do stuff with the loaded script!

    }

    // Add the script to the DOM
    (document.getElementsByTagName( "head" )[ 0 ]).appendChild( script );

    // Set the `src` to begin transport
    script.src = "https://remote.com/";
})();

hope that helps! cheers.

keeganwatkins
  • 3,636
  • 1
  • 14
  • 12
  • Is there a reason you manually set the script onload to null in the callback? Is there a problem with it triggering more than once, or is this just forcing some sort of garbage collection? – James Tomasino Oct 22 '13 at 13:50
  • 1
    Yep, that is to avoid memory leaks in old IE, due to the script element referencing a function as the onload handler. – keeganwatkins Oct 22 '13 at 16:06
2

Create a loader

You can inject the script in an orderly manner in a loader.

Beware that the execution of the dynamically loaded scripts usually comes after statically loaded scripts (i.e.<script src="My_script.js"></script>) (the order of injection in the DOM does not guarantee the opposite):

e.g., loader.js:

function appendScript(url){
   let script = document.createElement("script");
   script.src = url;
   script.async = false //IMPORTANT
   /*Node Insertion Point*/.appendChild(script);
}
appendScript("my_script1.js");
appendScript("my_script2.js");

my_script1.js will effectively be executed before my_script2.js, (helpful if dependencies of my_script2.js are in my_script1.js)

Note it's important to have script.async = false because dynamically loaded scripts has async = true by default, async does not assure you the order of loading.

Sheed
  • 577
  • 4
  • 18
0

Here is my adapted version, based on the answer of Frank with an additional expression to evaluate:

static async importScript(src, expressionToEvaluateAndReturn){

        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.async = true;
            script.src = src;
            script.addEventListener('load', (event)=>{
                if(expressionToEvaluateAndReturn){
                    try{
                        let result = eval(expressionToEvaluateAndReturn);
                        resolve(result);
                    } catch(error){
                        reject(error);
                    }
                    
                } else {
                    resolve();
                }
            });
            script.addEventListener('error', () => reject('Error loading script "' + src + '"'));
            script.addEventListener('abort', () => reject('Script loading aborted for "' + src + '"'));
            document.head.appendChild(script);
        });    
        
    }   

Example usage:

let d3 = await importScript('/bower_components/d3/d3.min.js','d3')
                    .catch(error => {
                        console.log(error);
                        throw error;
                    });
Stefan
  • 10,010
  • 7
  • 61
  • 117