Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Sara Dunnack
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Sara Dunnack

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

By
Published in Comments (9)

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.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

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

Thanks for posting this, works great!

1 Comments

Just wanted to say, thank you for this. I've been using $.append, $.load and $.html to execute a script tag from an AJAX response, and couldn't figure out why I was getting a timestamp added as a query string parameter to the script SRC network request. This clarified a lot- turns out jQuery was adding their Cachebuster.

I appreciate your careful documentation- now I'll find my fix!

1 Comments

Thanks, this was the solution for me after hours of banging my head against the wall trying variations of $("<InvalidTag></script>").on("load".... jQuery documentation is very weak/confusing in this area (ready() is only for document, .load() is deprecated or removed, etc).

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel