Surefire DOM Element insertion
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.documentElementcan 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:
- You are inserting a
<link>,<script>, or<img>(sumpthin' referencing another asset). - Said asset has a relative URL. (Yours has an absolute URL? You're A-OK, bud)
- The page has a
<base>element, changing the base URL used to resolve all relative URLs used. - The first
<script>element of the page is after said<base>element. - 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
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.
Thanks for sharing this pattern, but especially for the awesome mnemonic at the end.
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.
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.
doesn't this insert your element in the
<head>? or tag name script is just for example?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?
@Matt Bower
Yeah I documented it here because it's useful for everything. Writing your own getScript or JSONP handler is a good example.
@ionut
hmm yeah tossing it into head means it's display none. :|
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?
@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.
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'
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.
I highly doubt this will ever be a problem.
@Paul Irish no, i mean why use the 'script' tag as the reference?
@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.
Oh Paul, I really like the mnemonic for insertBefore and thanks to will for his heads-up with IE ;)).
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.
Thanks for the inspiration.
@Nick Sullivan
Doh. Last minute edit. Here's the correct version.
"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