Home > javascript > Surefire DOM Element insertion

Surefire DOM Element insertion

February 16th, 2011

So you've got an element. You need to add it to the DOM. Somewhere, anywhere, just be sure that it works.

In Modernizr, we need to do this all the time. In order to adequately do our feature detection we need to try out some elements in the DOM. But how exactly to add it? Steve Souders dug into appendChild vs insertBefore and the comments meandered through the sea of possibilities. Here's the best way to add yourElem:

var ref = document.getElementsByTagName('script')[0];
ref.parentNode.insertBefore(yourElem, ref);

But you prefer a good old document.body.appendChild(elem); Awww! I know, man. I used to, too.
Well, you can't always trust this.. Especially if you're writing library, widget or third-party code.

Here's why you can trust this insertBefore method, and why all the other techniques are not wise:

  • appendChild can throw Operation Aborted
  • <head> doesn't always exist in old opera, not-so-old safari, and others
  • if your script runs in the <head>, the <body> doesnt exist yet.
  • appending to document.documentElement can lead to an uncertain parentNode.
  • document.documentElement.firstChild (or .childNodes[0]) may be an html comment.
    • and as of FF4, insertBefore doesnt work with an html comment node

Okay okay okay, there is actually one case where the above method poses a problem:

  1. You are inserting a <link>, <script>, or <img> (sumpthin' referencing another asset).
  2. Said asset has a relative URL. (Yours has an absolute URL? You're A-OK, bud)
  3. The page has a <base> element, changing the base URL used to resolve all relative URLs used.
  4. The first <script> element of the page is after said <base> element.
  5. You don't want your element's asset path to be affected by the <base> element.

Wow. So if this scenario is a concern for you.. Then you need a small addition to the earlier code:

var ref = document.getElementsByTagName('base')[0] || document.getElementsByTagName('script')[0];
ref.parentNode.insertBefore(yourElem, ref);

But anyway that's a pretty narrow edge case so most people will be fine with the earlier snippet.

And if you have a hard time remembering the signature for insertBefore like I do, just think that your parent wants you to insert yourself before your younger brother. That's totally parentNode.insertBefore(you, sibling), right? :)

Thx souders, alex sexton, miketaylr, ben alman, jdalton & everyone else who cares about these small details

javascript

  1. February 16th, 2011 at 16:41 #1

    PS. this isn't news, nor is the technique recent. But this needed to be documented somewhere. :)

    Also if anyone is interested in digging into the effects of appending onto the documentElement some more, I'd be interested in seeing where that leads.

  2. Doug Avery
    February 16th, 2011 at 16:53 #2

    Thanks for sharing this pattern, but especially for the awesome mnemonic at the end.

  3. Imran
    February 17th, 2011 at 00:42 #3

    Nice blog.
    I have experienced that adding z-index style attr on element like positioned div doesn't work as well after document is ready. I solved the issue by wrapping another div using jquery wrap api. Can we do it in other better way?
    By the way I got the issue in poor IE7.

  4. February 17th, 2011 at 02:23 #4

    I first saw code like that (3rd party that needed a sure-fire way to insert a node) in Google Analytics, which is like the one in your example.

    My first thought was: "But what if I don't have any script on the page?!" But then I snapped to and realized how brilliant it is.

    And yes, it's details like these that make you smile.

  5. February 17th, 2011 at 04:19 #5

    doesn't this insert your element in the <head>? or tag name script is just for example?

  6. Matt Bower
    February 18th, 2011 at 14:15 #6

    Is this method primarily used for doing the feature testing for Modernizr or should it be used for things like JSONP and otherwise injecting DOM elements for other purposes?

  7. February 18th, 2011 at 14:28 #7

    @Matt Bower
    Yeah I documented it here because it's useful for everything. Writing your own getScript or JSONP handler is a good example.

  8. February 18th, 2011 at 14:32 #8

    @ionut

    hmm yeah tossing it into head means it's display none. :|

  9. Matt Bower
    February 18th, 2011 at 14:35 #9

    If you're injecting other script tags in the header, isn't the source order important? So if I have some JS inject script tags before it gets to loading the dependencies for those new scripts, wouldn't that break the system, or in those cases, should you attempt to fetch the next sibling element and insertBefore() that?

  10. February 18th, 2011 at 15:16 #10

    @Matt Bower

    Source order is not important no. The time at which a script is inserted into the DOM determines its position in the network request queue.. but that's about it..

    That's how script injection works, they are async.. Script tags in the original markup are executed synchronously, however.

  11. February 21st, 2011 at 09:35 #11

    Haha. Yes. I've been using this snippet only for the last recent months. I caught it too when inserting the new analytics code into twitter.com and thought 'hey, this is nice and small'

  12. February 21st, 2011 at 12:23 #12

    In Internet Explorer 7 (probably 6, maybe 8, hopefully not 9) an even narrower edge case can create a condition in which the NodeList is empty.

    smap= document.getElementsByTagName('script');
    isie= "v" == "\v";  
    var anchor= smap[0].parentNode;
    while (smap.length &gt; 0) 
      smap[0].parentNode.removeChild(smap[0]); 
     
    smap.length == 0;  // Expected; 
     
    anchor.appendChild (
       makeLocalScript("isIE &amp;&amp; smap.length == 0 ||  smap.length == 1");
    );

    I highly doubt this will ever be a problem.

  13. February 24th, 2011 at 04:40 #13

    @Paul Irish no, i mean why use the 'script' tag as the reference?

  14. February 24th, 2011 at 08:16 #14

    @ionut
    Because since this is operating in a script you can be guaranteed a script elem will be present.
    You can't be guaranteed any other element references will succeed.

  15. March 3rd, 2011 at 13:25 #15

    Oh Paul, I really like the mnemonic for insertBefore and thanks to will for his heads-up with IE ;)).

  16. Nick Sullivan
    May 10th, 2011 at 10:47 #16

    Hi – Love the idea, but I found a few issues with it when I ran it through my unit test suite that does a lot of intensive document.writing. Here's what I found, about 5-10% of the time.

    Chrome:
    Cannot read property 'parentNode' of undefined

    IE 8:
    'parentNode' is null or not an object

    Firefox 4.01
    scriptTag is undefined

    The above issues are all related, and it has to do with the fact that getElementsByTagName is a non standard array that arranges its contents dynamically – see http://userjs.org/help/tutorials/efficient-code

    My theory is that sometimes it tries to access the array via subscript and it fails. My fix, that has yet to reproduce the above errors, is to call the tags in a loop, so here's my version.

        var scripts = document.getElementsByTagName('script');
        for (var i = 0, len = scripts.len; i &lt; len; i++){
            if (scripts[i] &amp;&amp; scripts[i].parentNode){
                scripts[i].parentNode.insertBefore(t, scriptTag);
                break;
            }
        }

    Thanks for the inspiration.

  17. Nick Sullivan
    May 10th, 2011 at 10:54 #17

    @Nick Sullivan
    Doh. Last minute edit. Here's the correct version.

        var scripts = document.getElementsByTagName('script');
        for (var i = 0, len = scripts.len; i &lt; len; i++){
            if (scripts[i] &amp;&amp; scripts[i].parentNode){
                scripts[i].parentNode.insertBefore(t, scripts[i]);
                break;
            }
        }
  18. July 2nd, 2011 at 16:24 #18

    "appendChild can throw Operation Aborted"
    The funny thing is that MS fixed this for IE6/7/8 in a security update released in March 2010:
    http://support.microsoft.com/kb/974322

For code blocks, use <pre lang="javascript">. css and html4strict are also accepted.