Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Rick Stumbo
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Rick Stumbo

jQuery's append() Methods Intercept Script Tag Insertion And Circumvent Load Handlers

By Ben Nadel on

The other day, I was adding FunnelEnvy to an application. The FunnelEnvy library requires both a Script Tag injection and some subsequent configuration of the loaded module. And, since I was in the middle of some jQuery logic, I figured I would just use jQuery to create, append, and listen for the "load" event on said Script tag. But, it wasn't working. And, no matter how I altered the Script tag attributes or its properties, nothing was happening. I finally dropped out of jQuery and just used the native DOM (Document Object Model) APIs to get the job done. But, it didn't sit right with me. So, this morning, I dug into the jQuery source code to figure out where I was going wrong. And, what I discovered is that the jQuery append methods (append, appendTo, prepend, prependTo, etc.) all intercept Script Tag insertion into the DOM tree, thereby circumventing "load" event handlers. And, what's more, if you're using the slim build of jQuery, which omits the AJAX module, the Script Tag insertion into the DOM just fails silently.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Since I had no idea what was going wrong, I created a stand-alone demo using jQuery as the means to inject a Script tag. Then, I threw a "debugger;" statement at the top of the file and started stepping through each line of code as the control-flow passed down through the jQuery source code.

At first, nothing looked out of the ordinary. Then, after 10-minutes of step-debugging (jQuery does a LOT), I finally found the culprit! In the domManip() method that performs the final node insertion the collection of nodes is checked specifically for "script" elements. And, if a "script" element is in the collection, it is stripped out and passed through to jQuery._evalUrl() which, in turn, passes the request through to jQuery.ajax().

So, essentially, jQuery intercepts Script tag insertion and uses AJAX (Asynchronous JavaScript and JSON) to load the desired src location. By rerouting these requests, jQuery makes it much harder to see when the given script has been loaded into the active document.

NOTE: You could still use the global .ajaxSetup() method to setup a global success handler. Or, you could try using the .getScript() method to more explicitly load the remote source file.

To see this in action, I've put together a small demo which attempts to load various JavaScript files using a variety of element construction and injection techniques. Each of these techniques is [trying to] listen for the "load" event.

  • <!doctype html>
  • <html lang="en">
  • <head>
  • <meta charset="utf-8" />
  • <title>
  • jQuery's append() Methods Intercept Script Tag Insertion And Circumvent Load Handlers
  • </title>
  •  
  • </head>
  • <body>
  •  
  • <h1>
  • jQuery's append() Methods Intercept Script Tag Insertion And Circumvent Load Handlers
  • </h1>
  •  
  • <p>
  • <em>View console for script output.</em>
  • </p>
  •  
  • <!-- NOTE: The SLIM BUILD of jQuery omits the AJAX and Effects modules. -->
  • <script type="text/javascript" src="../../vendor/jquery/3.3.1/jquery-3.3.1.slim.js"></script>
  • <script type="text/javascript">
  •  
  • // TEST ONE: Manually constructing a Script Element and using the "onload"
  • // property to determine when the remote script has loaded.
  • (function() {
  •  
  • var scriptElement = document.createElement( "script" );
  •  
  • scriptElement.onload = function() {
  • console.log( "Successfully loaded script 1 using (onload)." );
  • };
  •  
  • scriptElement.src = "./external-script-1.js";
  • document.body.appendChild( scriptElement );
  •  
  • })();
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  • // TEST TWO: Manually constructing a Script Element and using the
  • // addEventListener() method to determine when the remote script has loaded.
  • (function() {
  •  
  • var scriptElement = document.createElement( "script" );
  •  
  • scriptElement.addEventListener(
  • "load",
  • function() {
  • console.log( "Successfully loaded script 2 using (addEventListener)." );
  • }
  • );
  •  
  • scriptElement.src = "./external-script-2.js";
  • document.body.appendChild( scriptElement );
  •  
  • })();
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  • // TEST THREE: Using jQuery to construct a Script Element and using (THIS IS MY
  • // POOR ASSUMPTION) the addEventListener() method to determine when the remote
  • // script has loaded.
  • (function() {
  •  
  • $( "<script>" )
  • .on(
  • "load",
  • function() {
  • console.log( "Successfully loaded script 3 using (jquery)." );
  • }
  • )
  • .prop( "src", "./external-script-3.js" )
  • .appendTo( document.body )
  • ;
  •  
  • })();
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  • // TEST FOUR: Manually constructing a Script Element and using the "onload" to
  • // determine when the remote script has loaded. Note that I am using jQuery,
  • // in this case, to do nothing more than the final APPEND to the document.
  • (function() {
  •  
  • var scriptElement = document.createElement( "script" );
  •  
  • scriptElement.onload = function() {
  • console.log( "Successfully loaded script 4 using (onload + jQuery)." );
  • };
  •  
  • scriptElement.src = "./external-script-4.js";
  •  
  • // jQuery is being used to do NOTHING MORE than append the element to the
  • // active document.
  • $( document.body ).append( scriptElement );
  •  
  • })();
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  • // TEST FIVE: Using jQuery to construct and configure the Script Element, but
  • // then injecting it into the DOM using vanilla DOM APIs.
  • (function() {
  •  
  • $( "<script>" )
  • .on(
  • "load",
  • function() {
  • console.log( "Successfully loaded script 5 using (jquery + appendChild)." );
  • }
  • )
  • .prop( "src", "./external-script-5.js" )
  • .each(
  • function() {
  • // We used jQuery to construct and configure the Script Element,
  • // but we're appending it using a vanilla DOM method.
  • document.body.appendChild( this );
  • }
  • )
  • ;
  •  
  • })();
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, some of these techniques use native DOM methods. Some use jQuery methods. And some use a combination of the two. And, when we run this code, here's what we see in the browser console:


 
 
 

 
 jQuery intercepts Script tag injection and re-routes control flow through an AJAX request. 
 
 
 

As you can see, tests THREE and FOUR fail completely. Not only do they not report any "load" event, they don't even attempt to load the remote scripts. This is because I am using the SLIM build of jQuery, which omits the AJAX module. And, if the AJAX module isn't present, jQuery intercepts the Script tag injection and then just silently bails out of the control-flow.

If I use the normal build of jQuery, which includes the AJAX module, and then re-run the page, we get a slightly different output:


 
 
 

 
 jQuery intercepts Script tag injection and re-routes control flow through an AJAX request. 
 
 
 

As you can see, with the normal build of jQuery, all five of the tests run and load scripts into the active document. However, the "load" event is only propagated to the three tests that did not use jQuery to actually append the Script tag to the DOM tree. That's because it's the append-action that actually intercepts the Script tags and re-routes the control flow (bypassing the load-event handlers).

I am sure that the jQuery library is doing this for a very meaningful reason. Everything that jQuery does is for some meaningful reason (that's why it's so awesome). But, this particular implementation really tripped me up. It would be great if the jQuery API documented that this was happening. Though, perhaps it does somewhere - it's been a really long time since I've read the jQuery docs.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Just what I looked for! Loading .js with jquery really gave me a headache.

Thanks for posting this, works great!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.