I scope, you scope, we all scope for NoScope! JS style element injection quirks in IE

The other day I was writing some updates and improvements for Modernizr, one to detect for generated content support and two to improve stylesheet and element injection. Modernizr already in a few places inserts a stylesheet and a corresponding element to do some tests e.g. generatedcontent, touch, css3transforms and a few others. All this happened multiple times; each test would inject an element and an inline style element, do its test then remove both elements. All this happens while the page is loading and as you can see the more tests that involve these steps exponentially grow the number of times it needs to touch the DOM.

Modernizrs’ previous method of injecting the elements revolves around forking for IE, IE<9 uses addRule() rather than insertRule(), this makes it hard to write a simple re-useable method for doing such a test that is cross browser. So I began thinking how this could be re-written to be simplified for re-usability. Rather than inject a style element into the head of a document we could use innerHTML and inject into the element that Modernizr will test against.

elem.innerHTML = "<style>"+css+"</style>";
doc.appendChild(elem);

Simple, easy and certainly re-usable. But something that stumped me for a while is IE6-9 would not append the style element inside the test element?

IE and the case of the NoScope element

After searching high and low I found some useful information about scope and NoScope elements in IE. Basically IE has a few elements that are considered NoScope which when trying to insert with innerHTML will be stripped! On this infamous page however it mentions if you precede a NoScope element with a scoped element they will not be stripped and we can then insert our style element.

elem.innerHTML = scopeElem+"<style>"+css+"</style>";
doc.appendChild(elem);

Huzzah! It works in IE6-9. We now have a simpler easier way to inject an element with associative styles and we’ve halved our touching of the DOM by bundling the test element and style element into one appendChild operation.

What’s considered a scoped element?

A scoped element can be anything other than a style, script or comment element. In Modernizrs’ case it uses the soft hyphen entity (&shy;) so as not to interfere with measurements done on the injected element. We could very well prepend the innerHTML string with the test element we wish to inject, but for easier referencing I create it in the method and pass it to the callback. This way we can keep the reference to the node and not have to query the DOM again.

injectElementWithStyles = function(rule,callback){
    //...
    var style, div = document.createElement('div');
    
    style = ['&shy;','<style>',rule,'</style>'].join('');
    div.id = mod;
    div.innerHTML += style;
    docElement.appendChild(div);

    ret = callback(div,rule);
    //...
}

There’s also a few other additions that centre around the injectElementWithStyles() method such as the test_bundle() method which does the DOM injection test in one fell swoop. If you want to know more go check out the source or hit me up on twitter.

Short URL: http://cssn.in/ja/033

 

Post filed under: javascript.

Skip to comment form.

  1. urielster says:

    thanks,this post helped me a l – o – t!!

    keep going!!

    urielster
    outbrain

  2. urielster says:

    small comment : you used ­ but it takes place on IE container (font size height) so the <style.. will appear only on the second line, why not to use for example and it takes no space at all…

    urielster
    outbrain

  3. I’m really stuck at the following:

    style = [‘­’,”,rule,”].join(”);

    Why would you join an array if you could as easily write the whole thing in a string at once? Isn’t this, however slightly, affecting performance? And if not performance, the total size of the script, even when compiled?

    If there’s a real plausible explanation, I’m all ears ;)

    • Ryan Seddon says:

      @Jan-Marten de Boer

      Yeah a straight concat would be faster overall but array joins actually perform better than straight concat in IE8<. Since IE's JScript engine is pretty slow favouring that would be optimal. Plus aesthetically it looks nicer.

  4. Stoyan says:

    this is awesome, thanks for the research and unearthing NoScope (our only hope, that helps us cope with IE’s slippery slope :)

    Anyway, I couldn’t get @import to work in a dynamic style text. Here’s a test case (the second piece of code):
    http://www.phpied.com/files/dynamic-styles/test.html
    Didn’t work for me in ie678

    Am I missing something obvious?

  5. Stoyan says:

    Thanks Ryan, I appreciate it.

    It sucks that when you have no idea what CSS you’re inserting, you’d then have to resort to some regex to figure out whether it contains @imports.

    The other way, of course is using cssText and create a stye element, instead of modifying innerHTML (http://www.phpied.com/dynamic-script-and-style-elements-in-ie/) to inject styles you don’t know of in advance. Again, care is required with IE and @import because if you modify cssText *before* you append a style element to the DOM and the css has an @import, IE will crash :)

    Thanks again, this ­ stuff is neat

    • Ryan Seddon says:

      @Stoyan –

      Yeah it does suck when you don’t know whats in the css being injected.

      There is a possible way to get around it. IE has the imports array like object that will contain any import declerations found in the injected stylesheet. You could check it on injection and see if there are any and then only use the addImport method if you find any values. But then again you might end up writing a lot of code, just to fix a potential edge case. This demo will output in the console the first import item

      Who knows why IE would acknowledge that there is an @import decleration but not load it.