Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Jessica Kennedy
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Jessica Kennedy

Scripts Tags Get Moved Only During Window DOM Node HTML Injection

By
Published in Comments (10)

Last week, I posted that performing HTML-based AJAX with jQuery moves Script tags around when injecting HTML into the DOM. At the time, it was unknown to me as to when this tag movement actually took place - when the raw HTML got parsed or when it was injected into the current document object model (DOM). After some further experimentation, it became clear that the Script tags were moved around during the actually DOM injection; simply parsing the raw HTML into a jQuery node collection did not have any affect. What this means is that you can still use the contextual features of Script-based meta data so long as you apply it pre-DOM-injection.

As you can see from the video, testing this consists of dynamically populating an unordered list (UL) using an AJAX request. The list items (LIs) that come back from the AJAX request each contain a Script-based meta data JSON value that should be applied to the parent element (the LI):

<li>
	<script type="text/x-json" class="meta-data">
		{
			id: 1
		}
	</script>
	<a>Click me to get ID</a>
</li>
<li>
	<script type="text/x-json" class="meta-data">
		{
			id: 2
		}
	</script>
	<a>Click me to get ID</a>
</li>
<li>
	<script type="text/x-json" class="meta-data">
		{
			id: 3
		}
	</script>
	<a>Click me to get ID</a>
</li>

Notice that there is nothing tying the Script tag to the parent node other than its context (ie. we're not using any REL attribute or other relational markers).

Here is the page that requests the above HTML via jQuery ajax:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
	<title>jQuery And Script Tags With AJAX Data</title>
	<script type="text/javascript" src="jquery-1.3.2.js"></script>
	<script type="text/javascript">

		// I am a jquery plugin that looks for script tags of class
		// meta-data and applies the JSON to its parent element in
		// the metaData key.
		jQuery.fn.applyMetaData = function(){
			// Find all the embedded script meta-data tags and
			// iterate of them to individually apply them to the
			// direct parent node.
			this.find( "script.meta-data" ).each(
				function( nodeIndex, scriptTag ){
					var script = $( this );
					var parent = script.parent();
					var metaData = {};

					// Try to evaluate the JSON meta data.
					try {
						metaData = eval( "(" + script.html() + ")" );
					} catch ( error ){
						// JSON was not valid.
					}

					// Store meta data into parent.
					parent.data(
						"metaData",
						jQuery.extend(
							{},
							parent.data( "metaData" ),
							metaData
							)
						);
				}
				)

				// Once the script tags have been processed,
				// remove them from the DOM.
				.remove()
			;

			// Return the collection without the script tags.
			return( this.not( "script.meta-data" ) );
		}


		// I handle the html data response from the AJAX request.
		function populate( htmlData ){
			var html = $( htmlData ).applyMetaData();

			// Put HTML into DOM.
			$( "#list" )
				.empty()
				.append( html )
			;

			// Bind click handlers.
			$( "#list a" )
				.attr( "href", "javascript:void( 0 )" )
				.click(
					function( clickEvent ){
						// Alert parent ID.
						alert( $( this ).parent().data( "metaData" ).id );

						// Prevent default event.
						return( false );
					}
					)
			;
		}


		// When DOM loads, initialize.
		$(
			function(){
				// Grab the remote data via AJAX request.
				$.ajax({
					type: "get",
					url: "script3_data.htm",
					dataType: "html",
					success: populate
					});
			}
			);

	</script>
</head>
<body>

	<h1>
		jQuery And Script Tags With AJAX Data
	</h1>

	<ul id="list" />

</body>
</html>

Notice that the first thing I do in the AJAX response handler - populate() - is to parse the HTML into a jQuery node collection and then immediately call the applyMetaData() plugin on the collection:

var html = $( htmlData ).applyMetaData();

Because simply parsing the HTML does not move the script tags around, we know that the call to $( htmlData ) will leave the Script tags in the same position they had in the raw HTML data. This fact allows the applyMetaData() plugin to search the resultant jQuery collection for Script tags of class "meta-data". Then, using context-only, the applyMetaData() plugin gets a reference to the parent element of the given Script tag and appends the Script's JSON data to the parent tag's metaData (using jQuery's data() method).

Once the jQuery applyMetaData() plugin has applied the Script tag JSON information, it strips the Script tags out of the transient DOM and then, for safe measure, returns a new jQuery collection that filters out any Script tags of class "meta-data". This resultant collection is then injected into the window DOM and the new link click events are bound to alert the ID of the their parent element's metaData id.

When I first learned about the Script-based meta data concept, it was the contextual nature of the Script tags that really blew my mind. When I thought last week that perhaps this contextual nature could not be used in conjunction with dynamically retrieved AJAX data, it definitely felt like a bit of a defeat. Knowing now, however, that the Script tag context is preserved in transient document object models and is only moved during primary DOM injection, I am feeling much more confident in this approach to meta-data delivery.

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

Reader Comments

2 Comments

It's so funny that I found the same issue on the same date of your post.

My colleague found out this is caused by jQuery not strictly checking for the type attribute. It is around Line 956 of jQuery-1.3.2 source.

Could this be considered a jQuery bug?

1 Comments

Around line 955 in jQuery v1.3.2:

if ( ret[i].nodeType === 1 )
ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
fragment.appendChild( ret[i] );

Replace it with:

if ( ret[i].nodeType === 1 ) {
var scriptChildren = jQuery.makeArray(ret[i].getElementsByTagName("script"))
for (var j = 0; j < scriptChildren.length; j++) {
if (scriptChildren[j].type && scriptChildren[j].type.toLowerCase() !== 'text/javascript') {
scriptChildren.splice(j, 1);
j--;
}
}
ret.splice.apply( ret, [i + 1, 0].concat(scriptChildren) );
}
fragment.appendChild( ret[i] );

This will solve the problem.

15,848 Comments

@Aaron, @Rainux,

Very interesting. I never even thought of looking in the jQuery library itself for the source of the issue! Hmmm. Yeah, I guess that would be considered a bug. Nice catch!

2 Comments

Ben,
Nice approach!
If we have schema AJAX - [XML] - XSLT - [XML] - [applyMetaData] - inject.
Both XSLT and [applyMetaData] performs some kind of transformation.
XSLT - do specific to request and [applyMetaData] is same for all requests.
Could anything be done on XSLT phase to simplify things?
I mean, it's good enough already, but still… out of curiosity?

15,848 Comments

@Oleg,

I am not sure. Since you need to apply the JSON via the jQuery data() method, I am not sure that the XSLT phase will add much.

2 Comments

Ben,
Thank for your reply.
In my schema AJAX calls XLM web service which is shared by many different applications.
XSL transformation is our standard phase - and that's how we decouple "data" from "presentation" (partially) and designers from programmers as well (almost 100% ;-).
Hence was my question to insert [applyMetaData] into XSL phase.
Obviously XSL cannot generate any java script objects…
Fortunately [applyMetaData] step is the same - so we have "no problemo" there!

15,848 Comments

@Oleg,

I don't understand 100%, but ultimately, applyMetaData() has to be executed in the Javascript, which part of the rendered content.

1 Comments

ok, so far I've come to the conclusion that jQuery 1.4.2 moves the script tags when parsed rather then on DOM insertion.

Also jQuery 1.4.2 has gone crazy where if you alert/console.log the return html from the ajax as a jquery object, you will see the script tags, but if you try to "grab" one, it will seemingly not exist.

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