Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at RIA Unleashed (Nov. 2009) with:

jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements

By Ben Nadel on

Yesterday, when I was playing around with using Script tags as data containers in jQuery-powered AJAX requests, I noticed some odd behavior in the way that the Script tags were brought across the wire to the client. In my testing, the Script tags were stripped out of the context in which they were originally written and added to the end of the AJAX request. I wanted to see is there was any rhyme or reason to the way in which this swap was executed, so I set up another test page. This one simply loads content from an external page into a DIV using 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">
  •  
  • // When the DOM has loaded, gather the html data.
  • $(
  • function(){
  • $.ajax(
  • {
  • method: "get",
  • url: "./script2_data.cfm",
  • dataType: "html",
  • success: function( strHTML ){
  • $( "#loader" ).html( strHTML );
  •  
  • // Log updated HTML.
  • console.log( $( "#loader" ).html() );
  • }
  • }
  • );
  • }
  • );
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • jQuery And Script Tags With AJAX Data
  • </h1>
  •  
  • <div id="loader"></div>
  •  
  • </body>
  • </html>

As you can see, when the DOM is ready to be interacted with, I load external HTML content into the target DIV and then log the resultant HTML to FireBug's console.

In my first test, I tried a number of different element combinations including inline, block, and nested display elements:

  • <!---
  • Here, we are going to load various HTML elements, all with
  • emebedded SCRIPT tags. This is to see how / when script tags
  • are stripped out and added to their parent container.
  • --->
  •  
  •  
  • <!--- Plain div. --->
  • <div>
  • <script type="text/plain">
  • Here is a DIV.
  • </script>
  • Here is a DIV.
  • </div>
  •  
  • <!--- Span inside div. --->
  • <div>
  • <span>
  • <script type="text/plain">
  • Here is a DIV / SPAN.
  • </script>
  • Here is a DIV / SPAN.
  • </span>
  • </div>
  •  
  • <!--- Plain span. --->
  • <span>
  • <script type="text/plain">
  • Here is a SPAN.
  • </script>
  • Here is a SPAN.
  • </span>
  •  
  • <!--- LI inside UL. --->
  • <ul>
  • <li>
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  • Here is a UL / LI.
  • </li>
  • <li>
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  • Here is a UL / LI.
  • </li>
  • </ul>
  •  
  • <!--- LI inside nested UL. --->
  • <ul>
  • <li>
  • <ul>
  • <li>
  • <script type="text/plain">
  • Here is a UL / UL / LI.
  • </script>
  • Here is a UL / UL / LI.
  • </li>
  • </ul>
  • </li>
  • </ul>

After the above HTML is retrieved via AJAX and loaded into the target DOM, the resultant HTML is as follows (spacing has been adjusted for readability):

  • <div>
  • Here is a DIV.
  • </div>
  • <script type="text/plain">
  • Here is a DIV.
  • </script>
  •  
  • <div>
  • <span>
  • Here is a DIV / SPAN.
  • </span>
  • </div>
  • <script type="text/plain">
  • Here is a DIV / SPAN.
  • </script>
  •  
  • <span>
  • Here is a SPAN.
  • </span>
  • <script type="text/plain">
  • Here is a SPAN.
  • </script>
  •  
  • <ul>
  • <li>
  • Here is a UL / LI.
  • </li>
  • <li>
  • Here is a UL / LI.
  • </li>
  • </ul>
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  •  
  • <ul>
  • <li>
  • <ul>
  • <li>
  • Here is a UL / UL / LI.
  • </li>
  • </ul>
  • </li>
  • </ul>
  • <script type="text/plain">
  • Here is a UL / UL / LI.
  • </script>

In this test, the Script tags were all stripped out and placed after the parent-most element in their respective clustering. Once I saw this parent-association pattern, I wanted to run the above test again, but this time with the entire test wrapped inside of a DIV tag, creating only a single parent-most element:

  • <!---
  • Here, we are going to load various HTML elements, all with
  • emebedded SCRIPT tags. This is to see how / when script tags
  • are stripped out and added to their parent container.
  • --->
  •  
  • <!--- Create only a single parent-most element. --->
  • <div>
  •  
  • <!--- Plain div. --->
  • <div>
  • <script type="text/plain">
  • Here is a DIV.
  • </script>
  • Here is a DIV.
  • </div>
  •  
  • <!--- Span inside div. --->
  • <div>
  • <span>
  • <script type="text/plain">
  • Here is a DIV / SPAN.
  • </script>
  • Here is a DIV / SPAN.
  • </span>
  • </div>
  •  
  • <!--- Plain span. --->
  • <span>
  • <script type="text/plain">
  • Here is a SPAN.
  • </script>
  • Here is a SPAN.
  • </span>
  •  
  • <!--- LI inside UL. --->
  • <ul>
  • <li>
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  • Here is a UL / LI.
  • </li>
  • <li>
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  • Here is a UL / LI.
  • </li>
  • </ul>
  •  
  • <!--- LI inside nested UL. --->
  • <ul>
  • <li>
  • <ul>
  • <li>
  • <script type="text/plain">
  • Here is a UL / UL / LI.
  • </script>
  • Here is a UL / UL / LI.
  • </li>
  • </ul>
  • </li>
  • </ul>
  •  
  • </div>

This time, the resultant HTML after the AJAX data injection looked like this:

  • <div>
  •  
  • <div>
  • Here is a DIV.
  • </div>
  •  
  • <div>
  • <span>
  • Here is a DIV / SPAN.
  • </span>
  • </div>
  •  
  • <span>
  • Here is a SPAN.
  • </span>
  •  
  • <ul>
  • <li>
  • Here is a UL / LI.
  • </li>
  • <li>
  • Here is a UL / LI.
  • </li>
  • </ul>
  •  
  • <ul>
  • <li>
  • <ul>
  • <li>
  • Here is a UL / UL / LI.
  • </li>
  • </ul>
  • </li>
  • </ul>
  •  
  • </div>
  •  
  • <script type="text/plain">
  • Here is a DIV.
  • </script>
  •  
  • <script type="text/plain">
  • Here is a DIV / SPAN.
  • </script>
  •  
  • <script type="text/plain">
  • Here is a SPAN.
  • </script>
  •  
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  •  
  • <script type="text/plain">
  • Here is a UL / LI.
  • </script>
  •  
  • <script type="text/plain">
  • Here is a UL / UL / LI.
  • </script>

Now, we're really starting to see the pattern - the script tags are stripped out of the entire AJAX HTML dataset and added after the parent-most element in the AJAX data DOM. If there is no single parent-most element, then as we saw in the first demo, the Script tags are simply added after the parent-most element of the given sub-tree.

It appears that neither the type of nor the position of the parent element has any impact on the way that the Script tags are inserted into the target DOM. As such, I think that the only viable solution for using Script tags as application data containers is to use the REL attribute to store the unique ID of the target node. Of course, this only applies to HTML that is gotten via AJAX - Script tags that are embedded in the original page rendering keep their original context.



Looking For A New Job?

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

Reader Comments

Well after playing around with it for a couple of days, I've actually changed my mind - I really like your technique. It has immensely simplified a few things that I was jumping through hoops to get done before, like adding pop-up help text. I used to use all kinds of nasty jQuery hackery to pass the text from the db to a jQuery UI dialog. Now I just dump it in a script tag and then load that script tag into the popup (referencing it by a DOM id).

The contextualization really wasn't that important to the way I would use this - I'm much more comfortable with the paradigm of tagging items (using rel tags or ids) anyway. But the overall concept is still a really nice one.

Reply to this Comment

@Roland,

Awesome my man; glad you are liking this approach. I really like it, although the context issues threw me through a loop. But, as long as you are cool with the IDs, I think it makes good sense.

Reply to this Comment

The script tag being used for data seems rather interesting. I haven't had much time to test this myself, so would like to pose the question: Does the script tag movement happen on the AJAX call or on the DOM insertion?

So what would happen if you took out the AJAX call?

And if it is the AJAX call that is re-ordering; is it possible to encrypt or read as binary to by-pass the parsing of the file? (I am not sure what security issues may arise from this though).

Reply to this Comment

Sorry, this is off topic...

Ben, how do you pronounce your last name?

NahDAHL?
NayDELL?

etc....

Reply to this Comment

@eXcalibur.lk,

Good question - the script movement is actually happening on the DOM insertion. The data that comes back over the wire is kept as-is.

@Ivan,

It's prounounced, "Nay-Dell"

Reply to this Comment

@eXcalibur.lk,

It's probably the browser. I can't see why jQuery would do that on purpose. Furthermore, I wouldn't be surprised if this behavior was NOT cross-browser consistent (although I have not tested it).

Reply to this Comment

@Ben,

I have finally had time to sit down and test it out; and it does look like JQuery is moving it around.

The following script:
<script type="application/javascript">
$(document).ready(
function(){
var html = '<div><script type="text/plain">Here is a DIV.<\/script>Here is a DIV.</div>';
document.getElementById('content').innerHTML = html;
console.log(document.getElementById('content').innerHTML);

$('#contentJQuery').html(html);
console.log($('#contentJQuery').html());
}
);
</script>
</head>

<body>
<div id="content"></div>
<div id="contentJQuery"></div>
</body>
</html>

Outputs:
# <div><script type="text/plain">Here is a DIV.</script>Here is a DIV.</div>
# <div>Here is a DIV.</div><script type="text/plain">Here is a DIV.</script>

Reply to this Comment

@eXcalibur.lk,

Other than losing the context, I'm not finding any functional issues with this, so, at least that's good.

Reply to this Comment

After some further testing, it looks like this Script tag shifting was happening ONLY when the html was injected into the window's primary DOM tree. Parsing the raw HTML data (gotten via AJAX) into a transient DOM tree (not yet rendered) leaves the Script tags in their original context!!

http://www.bennadel.com/blog/1616-Scripts-Tags-Get-Moved-Only-During-Window-DOM-Node-HTML-Injection.htm

This means that as long as the meta-data is applied pre-rendering, the context feature should be leveraged. Fantastic news.

Reply to this Comment

Hey Ben, I'm having an issue related to this.

I'm loading external content into my page using jQuery, and the external content includes script tags. However, once the content is loaded, it appears that the script tags are completely lost.

Basically, my external content contains flash video that is embedded with SWFObject. When I retrieve this content with jQuery (using the .load() method) i basically have no flash video in the retrieved content.

Any idea how to keep the script tags so they are executed without having to resort to plain ol' html <object> tags?

Thanks!

Reply to this Comment

Other than losing the context, I'm not finding any functional issues with this, so, at least that's good.

Reply to this Comment

The docs for the ajax function states that:

"If html is specified, any embedded JavaScript inside the retrieved data is executed before the HTML is returned as a string."

This may explain why JQuery re-arranges the order of the script content.

Reply to this Comment

encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API needs as script tags. These script tags are moved to the end of the inserted content and therefor the Linkedin share button is rendered in the incoorect position.

Now I do need to inject them so I'm still on the lookout for a workaround as the non standard <in:tag is not working in IE...

Cheers

Reply to this Comment

Update! Instead of

  • $(eval(options.insertAfter)).after(data['insertData']);

I now use:

  • var ajaxNode = document.createElement('span');
  • var parent = $(eval(options.insertAfter))[0].parentNode;
  • ajaxNode.innerHTML = data['insertData'];
  • parent.insertBefore(ajaxNode, $(eval(options.insertAfter))[0].nextSibling);

Works perfectly and proves again this is a jQuery issue!

Cheers

Reply to this Comment

I just ran into this as well. I know it's frustrating, but after I thought about it, I am going to assume that whether it's jQuery or the browser doing it, the reason is likely to ensure that any JS in the DOM insertion is executed after its (apparently) relevant/dependent HTML.

FYI, I'm in Chrome, and it looks like even the following nested quickie-code will result in two same-level nodes.

  • console.log($("<div class='test'><script></script></div>"));

Reply to this Comment

To the extent that any of the script contains data provided by a user, this technique seems to invite XSS exploits. With sufficient vigilance you can probably prevent the attack by other means, but I would be really nervous about exercising sufficient vigilance. Most software developers and organizations don't.

Reply to this Comment

I have probably overreached in my previous statement. Better: "Too many software developers and organizations don't" .... exercise sufficient vigilance against exploits.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.