Scripts Tags Get Moved Only During Window DOM Node HTML Injection

Posted June 19, 2009 at 10:17 AM by Ben Nadel

Tags: Javascript / DHTML

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.




Reader Comments

Jun 22, 2009 at 3:16 AM // reply »
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?


Jun 22, 2009 at 3:46 AM // reply »
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.


Jun 22, 2009 at 4:18 AM // reply »
2 Comments

I've opened a ticket and a patch:

http://dev.jquery.com/ticket/4801


Jun 23, 2009 at 5:58 PM // reply »
11,238 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!


Jun 23, 2009 at 9:33 PM // reply »
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?


Jun 24, 2009 at 8:21 AM // reply »
11,238 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.


Jun 24, 2009 at 2:42 PM // reply »
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!


Jun 24, 2009 at 3:25 PM // reply »
11,238 Comments

@Oleg,

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


Jul 19, 2010 at 12:05 PM // reply »
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.


Jul 20, 2010 at 9:26 PM // reply »
11,238 Comments

@Liam,

Hmm. I wonder if this behavior has switched in the latest releases of jQuery.


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 21, 2013 at 11:51 AM
Ask Ben: Parsing Very Large XML Documents In ColdFusion
Looking at my first ever XML document that I have to parse and put into MS SQL 2000 with CF8. I get it to list the desired Field name, many times over, and have a long list of this field name displa ... read »
May 21, 2013 at 9:25 AM
Turning Off and On Identity Column in SQL Server
you are awesome..i am lucky to get this blog between such a garbage one....Thanks, Prashant ... read »
May 20, 2013 at 4:38 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, Your confusion is well founded, since this is a very confusing features. In fact, it ONLY works if you use array notation. Meaning, that this: arrayToList( query[ "columnName" ] ) ... read »
May 20, 2013 at 4:34 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I was thinking chicken and the egg, I wouldn't have expected it to work in the valuelist going in I guess. Maybe I just need a beer, long day :) ... read »
May 20, 2013 at 4:29 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, That's if you're trying to reference a specific row. In this case, we're trying to reference the entire query column as one cohesive value. So, you are correct that if you wanted to output a ... read »
May 20, 2013 at 4:24 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I thought when you used array notation to reference queries you always had to have the row or it would throw a similar error as well? ... read »
May 20, 2013 at 11:45 AM
Using jQuery's Animate() Step Callback Function To Create Custom Animations
This is really useful. I found out that you don't actually have to use a dummy css property (surprisingly). To animate a property in a linear-gradient for instance I did this this.css('someLinearGra ... read »
May 20, 2013 at 10:51 AM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Josh, Oh snap! You're totally right! I'm not sure I've ever tried that. I did know that you can call a number of other array-methods on ColdFusion query columns: http://www.bennadel.com/blog/167 ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools