tags:

views:

3553

answers:

13

How can you reliably and dynamically load a javascript file? This will can be used to implement a module or component that when 'initialized' the component will dynamically load all needed javascript library scripts on demand.

The client that uses the component isn't required to load all the library script files (and manually insert tags into their web page) that implement this component - just the 'main' component script file.

How do mainstream javascript libraries accomplish this (Prototype, jquery, etc)? Do these tools merge multiple javascript files into a single redistributable 'build' version of a script file? Or do they do any dynamic loading of ancillary 'library' scripts?

An addition to this question --- is there a way to handle the event after a dynamically included javascript file is loaded? Prototype has document.observe for document-wide events. Example:

document.observe("dom:loaded", function() {
  // initially hide all containers for tab content
  $$('div.tabcontent').invoke('hide');
});

What are the available events for a script element?

+2  A: 

Here is some example code I've found... does anyone have a better way?

  function include(url)
  {
    var s = document.createElement("script");
    s.setAttribute("type", "text/javascript");
    s.setAttribute("src", url);
    var nodes = document.getElementsByTagName("*");
    var node = nodes[nodes.length -1].parentNode;
    node.appendChild(s);
  }
Adam
A: 

all the major javascript libraries like jscript, prototype, YUI have support for loading script files. For example, in YUI, after loading the core you can do the following to load the calendar control

var loader = new YAHOO.util.YUILoader({

    require: ['calendar'], // what components?

    base: '../../build/',//where do they live?

 //filter: "DEBUG",  //use debug versions (or apply some
      //some other filter?

 //loadOptional: true, //load all optional dependencies?

 //onSuccess is the function that YUI Loader
 //should call when all components are successfully loaded.
    onSuccess: function() {
  //Once the YUI Calendar Control and dependencies are on
  //the page, we'll verify that our target container is 
  //available in the DOM and then instantiate a default
  //calendar into it:
  YAHOO.util.Event.onAvailable("calendar_container", function() {
   var myCal = new YAHOO.widget.Calendar("mycal_id", "calendar_container");
   myCal.render();
  })
     },

    // should a failure occur, the onFailure function will be executed
    onFailure: function(o) {
        alert("error: " + YAHOO.lang.dump(o));
    }

 });

// Calculate the dependency and insert the required scripts and css resources
// into the document
loader.insert();
Darren Kopp
+8  A: 

I did basically the same thing that you did Adam, but with a slide modification to make sure I was appending to the head tag to get the job done. I simply created an include function (code below) to handle both script and css files.

This function also checks to make sure that the script or CSS file hasn't already been loaded dynamically. It does not check for hand coded values and there may have been a better way to do that, but it served the purpose.

function include( url, type ){
    // First make sure it hasn't been loaded by something else.
    if( Array.contains( includedFile, url ) )
        return;

    // Determine the MIME-type
    var jsExpr = new RegExp( "js$", "i" );
    var cssExpr = new RegExp( "css$", "i" );
    if( type == null )
        if( jsExpr.test( url ) )
            type = 'text/javascript';
        else if( cssExpr.test( url ) )
            type = 'text/css';

    // Create the appropriate element.
    var tag = null;
    switch( type ){
        case 'text/javascript' :
            tag = document.createElement( 'script' );
            tag.type = type;
            tag.src = url;
            break;
        case 'text/css' :
            tag = document.createElement( 'link' );
            tag.rel = 'stylesheet';
            tag.type = type;
            tag.href = url;
            break;
    }

    // Insert it to the <head> and the array to ensure it is not
    // loaded again.
    document.getElementsByTagName("head")[0].appendChild( tag );
    Array.add( includedFile, url );
}
palehorse
includeFile is not defined.
Mike Bethany
Without more context than that Pickle, I'm afraid I don't have any suggestions. The code above works as-is, it was pulled directly from a functioning project.
palehorse
A: 

does anyone have a better way?

I think just adding the script to the body would be easier then adding it to the last node on the page. How about this:

function include(url) {
  var s = document.createElement("script");
  s.setAttribute("type", "text/javascript");
  s.setAttribute("src", url);
  document.body.appendChild(s);
}
Joseph Pecoraro
A: 

Is Rhino one of these tools?

Rhino is just a JavaScript Engine. Over at Wikipedia they say that (JavaScript_engine)">Rhino is one of Mozilla's ECMA-262 Edition 3 compliant JavaScript Engines. Its most unique quality is that it was written in Java and is open source. You can download the free Java libraries and run a Rhino Javascript Shell right from your terminal. This is quite useful. You can do the same with (Javascript_engine)">SpiderMonkey another Mozilla JavaScript engine that is written in C.

Joseph Pecoraro
A: 

I guess they have tools that merge multiple javascript files into a single redistributable 'build' version of a script file?

This may be a little off topic but I'll throw it out there.

There is a neat way that you can do this on the server side. If you wanted to do this dynamically you would craft a request to the server's script listing the files on in the URL like query parameters. The server script can than combine the scripts together and returns them in a single script.

The one that I am most familiar with can be found by googling combine.php. That article goes above and beyond by using URL Rewriting to make the syntax easier to construct, but the idea still stands that you can reduce the size of files and reduce the number of requests. As always you should be gzipping javascript responses.


Sorry for the multiple posts. It just seems like there are multiple questions =)

Joseph Pecoraro
+1  A: 

The technique we use at work is to request the javascript file using an AJAX request and then eval() the return. If you're using the prototype library, they support this functionality in their Ajax.Request call.

17 of 26
A: 

If you attempt to load a JavaScript file outside of the head tag you will break w3c and xhtml standards.

You will also lose Opera and konqueror support.

mmattax
Where did you hear this? Loading scripts outside the head element is fine. To quote the W3C, "This (script) element may appear any number of times in the HEAD or BODY of an HTML document."
thomasrutter
A: 

@mmattax: it may break Opera but I'm not exactly sure that being outside the head breaks XHTML standards. I just downloaded the XHTML Strict 1.0 Doctype Definition and got the following (abbreviated to just the important parts):

<!ELEMENT body %Block;>
<!ENTITY % Block "(%block; | form | %misc;)*">
<!ENTITY % misc "noscript | %misc.inline;">
<!ENTITY % misc.inline "ins | del | script">

That shows that a script tag is valid inside the body. Did I miss something?

Joseph Pecoraro
+3  A: 

I used a much less complicated version recently with jQuery:

<script src="scripts/jquery.js" type="text/javascript"></script>
<script type="text/javascript">//<![CDATA[
  var js = ["scripts/jquery.dimensions.js", "scripts/shadedborder.js", "scripts/jqmodal.js", "scripts/main.js"];
  var $head = $("head");
  for (var i = 0; i < js.length; i++) $head.append("<script src=\"" + js[i] + "\" type=\"text/javascript\"></scr" + "ipt>");
//]]></script>

It worked great in every browser I tested it in: IE6/7, Firefox, Safari, Opera.

travis
That's great... unless you're trying to load jquery.
Mike Bethany
+1  A: 

i've used yet another solution i found on the net ... this one is under creativecommons and it checks if the source was included prior to calling the function ...

you can find the file here: include.js

/** include - including .js files from JS - [email protected] - 2005-02-09
 ** Code licensed under Creative Commons Attribution-ShareAlike License 
 ** http://creativecommons.org/licenses/by-sa/2.0/
 **/              
var hIncludes = null;
function include(sURI)
{   
  if (document.getElementsByTagName)
  {   
    if (!hIncludes)
    {
      hIncludes = {}; 
      var cScripts = document.getElementsByTagName("script");
      for (var i=0,len=cScripts.length; i < len; i++)
        if (cScripts[i].src) hIncludes[cScripts[i].src] = true;
    }
    if (!hIncludes[sURI])
    {
      var oNew = document.createElement("script");
      oNew.type = "text/javascript";
      oNew.src = sURI;
      hIncludes[sURI]=true;
      document.getElementsByTagName("head")[0].appendChild(oNew);
    }
  }   
}
Pierre Spring
+5  A: 

You may write dynamic script tags (using Prototype):

new Element("script", {src: "myBigCodeLibrary.js", type: "text/javascript"});

The problem here is that we do not know when the external script file is fully loaded.

We often want our dependant code on the very next line and like to write something like:

if (iNeedSomeMore){
  Script.load("myBigCodeLibrary.js");  // includes code for myFancyMethod();
  myFancyMethod();                     // cool, no need for callbacks!
}

There is a smart way to inject script dependencies without the need of callbacks. You simply have to pull the script via a synchronous AJAX request and eval the script on global level.

If you use Prototype the Script.load method looks like this:

var Script = {
  _loadedScripts: [],
  include: function(script){
    // include script only once
    if (this._loadedScripts.include(script)){
      return false;
    }
    // request file synchronous
    var code = new Ajax.Request(script, {
      asynchronous: false, method: "GET",
      evalJS: false, evalJSON: false
    }).transport.responseText;
    // eval code on global level
    if (Prototype.Browser.IE) {
      window.execScript(code);
    } else if (Prototype.Browser.WebKit){
      $$("head").first().insert(Object.extend(
        new Element("script", {type: "text/javascript"}), {text: code}
      ));
    } else {
      window.eval(code);
    }
    // remember included script
    this._loadedScripts.push(script);
  }
};
aemkei
A: 

Just found out about a great feature in YUI 3 (at the time of writing available in preview release). You can easily insert dependencies to YUI libraries and to "external" modules (what you are looking for) without too much code: YUI Loader.

It also answers your second question regarding the function being called as soon as the external module is loaded.

Example:

YUI({
    modules: {
     'simple': {
      fullpath: "http://example.com/public/js/simple.js"
     },
     'complicated': {
      fullpath: "http://example.com/public/js/complicated.js"
      requires: ['simple']  // <-- dependency to 'simple' module
     }
    },
    timeout: 10000
}).use('complicated', function(Y, result) {
    // called as soon as 'complicated' is loaded
    if (!result.success) {
     // loading failed, or timeout
     handleError(result.msg);
    } else {
     // call a function that needs 'complicated'
     doSomethingComplicated(...);
    }
});

Worked perfectly for me and has the advantage of managing dependencies. Refer to the YUI documentation for an example with YUI 2 calendar.

Kariem