Insert in place without document.write

So here’s the situation: you want to syndicate some content using JavaScript to pull in the data (like AdWords or similar programs). The syndication script can’t know anything about the page it’s being used on — so it can’t have a dependency on the existence of particular elements. Yet the host page needs to be able to control where the content is inserted — the syndication script needs to insert the content wherever the <script> tag is.

How do you do it?

Well you probably do what Google does and use document.write. But document.write is not a nice technique, it has some notable issues:

  • document.write does not work in XHTML mode (on XHTML pages served as XML)
  • content written in with document.write may not subsequently appear in the page’s DOM, which means no further access to manipulate it programmatically, and no access to accessibility APIs
  • document.write is conceptually wrong because it treats nodes as serialized text — which they’re not — they’re nodes

But what’s the alternative? DOM creation techniques need an existing element reference to work with; even innerHTML needs to know where to write the HTML. And in both cases, simply appending to <body> is not an option if you want the new content to appear inline with the existing content.

I was faced with this dilemma a few days ago, until an obvious solution dawned on me — we do in fact have a predictable refence: the <script> element itself!

All we need is a way of identifying the including <script> in the DOM, and then we can use the insertBefore method to append new HTML directly before it.

So, given a syndication script with a fixed ID:

<script type="text/javascript" id="syndication" src="syndication.js"></script>

We can go from oldskool nastiness like this:

document.write('<p id="syndicated-content">Here is some syndicated content.</p>');

To modern loveliness like this:

var newcontent = document.createElement('p');
newcontent.id = 'syndicated-content';
newcontent.appendChild(document.createTextNode('Here is some syndicated content.'));

var scr = document.getElementById('syndication');
scr.parentNode.insertBefore(newcontent, scr);

We could even go a step further and remove the <script> ID, but in that case we would need a concrete method for identifying the specific element. We could do that by knowing its SRC:

var scripts = document.getElementsByTagName('script');
for(var i=0; i<scripts.length; i++)
{
    if(scripts[i].src == 'http://www.mydomain.com/syndication.js')
    {
        //scripts[i] is the one

        break;
    }
}

And there you have it – a simple but elegant way of inserting content in place, removing the last vestige of need for document.write!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.sitepoint.com/ Kevin Yank

    This is one of those forehead-smackingly obvious solutions you wonder why nobody thought of sooner. Wicked stuff, James!

    Now if only we could get Google to adopt this in AdSense. It would be far better than the hacks people are currently using to support document.write in XHTML.

  • Stuart Langridge

    Better still, if you enumerate document.getElementsByTagName(‘script’), the last one in the array is the script element you’re in.

  • Stefan Scholl

    The id should be more unique. “syndication” is a bad example. Some people like to clutter their site with more than one external script element.

  • http://www.brothercake.com/ brothercake

    @Stuart: D’oh of course! If the script is non-deferred, then the DOM will stop building at that point until it’s processed, so by definition it will be the last member in the scripts collection. Nice one :)

    [EDIT: apparently that's not the case in XHTML mode - according to the page that Kevin linked to, in XHTML mode the DOM is created first and then scripts are processed]

    Another idea I had was to pre-process the script itself with PHP, then it would have a reference to its own SRC without you having to know it in advance:

    <?php 
    $scripturi = $_SERVER['SCRIPT_URI'];
    if(!empty($_SERVER['QUERY_STRING'])) { $scripturi .= '?' . $_SERVER['QUERY_STRING']; }
    ?>
    var scripts = document.getElementsByTagName('script');
    for(i=0; i<scripts.length; i++)
    {
    	if(scripts[i].src == '<?php echo $scripturi; ?>')
    	{
    		scripts[i].parentNode.insertBefore(newcontent, scripts[i]);
    		break;
    	}
    }
    
    

    But that of course is less than ideal since it’s not a pure JS solution.

    @Stefan: sure, it’s an over-simplified example :)

  • http://www.sitepoint.com/ Kevin Yank

    Yep, as the page I linked to explains, there are plenty of ways to do without document.write, but none of the obvious ones work in current browsers’ XHTML DOMs. That’s why James’s proposal is so inspired.

  • jeresig

    @brothercake: That will only help you if you’re actively trying to not use document.write() from the start (which, quite frankly, is trivial to do in this day and age). The larger issue is dealing with scripts that use document.write(), causing it not to work in XHTML documents.

    As Stuart mentioned, getElementsByTagName(“script”) does work – and it works in XHTML documents, it just has issues in Firefox. You can read my full analysis on the issue, and working solution for Google Adsense (and Google Maps) here – working in all modern browsers that support XHTML served with the correct mimetype:
    http://ejohn.org/blog/xhtml-documentwrite-and-adsense/

  • Stuart Langridge

    Ha! I knew someone had written this up recently but couldn’t remember who; it was John :-)

  • Badcop666

    Hey, I suggest that running code inline is generally bad practice – especially code which accesses or modifies the DOM. Initialising data structures, objects etc seems to be fine – but anything else should be deferred using onload or some similar technique.

    I use the following:-

    var initList=[]; //global var inline in the page.

    Then, in any included script file or code block I put as the last line:-

    initList.push( thisInit );
    

    Where “thisInit” is the name of a initialisation function.

    I refer to this as “registration”. If you wanted to be really elegant, I guess you could wrap it in a class – perhaps using a custom “onload” object something like:-

    onload.register( thisInit );
    

    the body tag then includes the following:-

    
    

    or something similar, which simply iterates over the array, calling each function thus:-

    for(x=0;...)
    {
       initList[ x ]();
    }
    

    There are things to consider if you are working with someone else’s approach to onload handlers, where you might need to do the horrile:-

    oldOnload = document.body.onload;
    document.body.onload = myNewOnload();

    in which case you’d simply need to do this once – attaching your nice scaleable solution. I suggest that this is a scaleable solution which works with any number of external libraries or code blocks.

    Further, and more specifically to this situation, I’d suggest that tagging each code block so it can look for itself in the DOM is a really good idea – a naming convention which allows you to use a simple regexp to pull out the id of the code block your code is in. I have been using the scripts[scripts.length-1] approach up to now, and wasn’t aware of the xhtml limitation – thanks for that!

    badcop666_at_hot_mail_dot_com

  • http://www.sitepoint.com/ Kevin Yank

    @jeresig:

    That will only help you if you’re actively trying to not use document.write() from the start (which, quite frankly, is trivial to do in this day and age).

    I agree that this largely comes down to an advocacy issue. If we can convince the Googles of the world to abandon document.write() and use clean solutions like the one in this post, it will solve a lot of problems.

  • http://diigital.com cranial-bore

    It’s so simple it just might work! Nice.

  • hjess

    This might be an issue for some; in HTML 4.01 documents (not XHTML),

    ID is not a valid attribute for the SCRIPT element.

  • Sunny

    Is it so hard to have a <script...>>/script<<div id="syndication"><div> ?

    This way not only does it comply to HTML 4.01 but it also lets one put the “script” part in the page header.

  • Sunny

    (Obviously intended code was <script ...></script><div id="syndication"></div>)

  • http://www.brothercake.com/ brothercake

    @Sunny, sure that’s an option, but I was looking for something that doesn’t place an additional requirement on the host (even a tiny one)

    My original purpose for this was content syndication for the upcoming contests and marketplace re-launch, and a way of syndicating that was so simple, all we had to say to people is “put this script on your page”. So I went for the hybrid PHP solution, where the script knows its own URL definitively (including any query parameters), and hence no additional elements are required and the script doesn’t even need an ID.

  • Etnu

    Better yet — stop wasting your time with the effectively dead technology that is xhtml. It was an interesting idea, but lets face it — without IE support, you’re going to be serving html4 anyway. HTML5 will be supported before xhtml 1.0 is.

  • http://www.brothercake.com/ brothercake

    XHTML in IE is just HTML4 with extra slashes – sure.

    But in decent browsers it’s XML. And guess what – it’s the same markup.

    XHTML works fine; as a transitional stage from SGML-style markup that’s needlessly complex to parse and error-correct, to XML-style markup that’s simple and easy to parse; and as the XML itself.

  • http://www.sitepoint.com/ andrew.k

    We never had these problems with ASCII. :P

    EOF

  • Ara Pehlivanian

    I do it a bit differently because an ID isn’t legal in a script tag. Also, when using 3rd party scripts, you get into trouble when they have multiple document.write()s per provider. (http://arapehlivanian.com/2006/05/12/documentwrite-fix-for-real-xhtml/)

  • Anonymous

    This is great, but if my text is HTML, it doesn’t render.

    How would you apply a css class to the new text?

  • Anonymous

    yeah, i have to go figure out how to apply class and for attributes… guessing theres a namespace problem, i hope there is a way around.

  • Anonymous

    @above: nodeName.setAttribute(‘for’,'text’)

  • http://www.nvest.ru Forexman

    Hi. This is really interesting post. Thank You! I have just subscribed to Your rss!

    Best regards

  • Anonymous

    page content addition

    function addHTML (html) {
    if (document.all)
    document.body.insertAdjacentHTML(‘beforeEnd’, html);
    else if (document.createRange) {
    var range = document.createRange();
    range.setStartAfter(document.body.lastChild);
    var docFrag = range.createContextualFragment(html);
    document.body.appendChild(docFrag);
    }
    else if (document.layers) {
    var l = new Layer(window.innerWidth);
    l.document.open();
    l.document.write(html);
    l.document.close();
    l.top = document.height;
    document.height += l.document.height;
    l.visibility = ‘show’;
    }
    }

    <input type=”button”
    onclick=”addHTML(‘‘ + new Date() + ”);”
    value=”add current date”
    />

  • Juan Vazquez-Abarca

    James, great stuff….

    However i’m wondering using this option how do i call for example a CSS file for that piece of script.

    Thanks