Last month I posted general guidelines for writing cross-browser code. Specifically I emphasized that feature detection (rather than browser detection) is better for working around differences between browsers. This is because feature detection automatically adapts to what a given browser supports and thus deals gracefully with new releases. Browser detection requires researching the level of support in every version of every browser and must be updated each time a new browser is released. Today I want to dig into a real-world example of this from some recent discussions. The conversation began around a piece of code like the following:
// DON'T USE THIS
/*@cc_on
@if( @_jscript_version < 9 )
// Enable styling of new HTML5 elements
...
@end
@*/
The goal of this code is to detect and work around a missing feature. In this case the feature is styling new HTML5 elements, or more specifically, using CSS to customize the display of new HTML5 elements. As written, this code fails to run in IE9 Compatibility View and ends up leaving new HTML5 elements without styles. In IE9 Standards Mode this is not a problem as IE9 enables styling for all elements by default.
The first problem that leads to this effect is the use of JScript’s conditional compilation which is similar to conditional comments. Both of these are forms of browser detection and should generally be avoided, especially when targeting the latest version of a browser. The second and more serious problem in this code is an attempt to use the @_jscript_version
statement to detect the document mode of the page. The @_jscript_version
statement is actually an indicator of which version of JScript is in use by the browser as a whole. In ALL document modes of IE9, this statement currently equates to “9”. In ALL document modes of IE8 it equates to “5.8” and in IE7 it is “5.7”. Thus it is not the correct piece of information to use to determine document mode.
Fortunately, developers can determine document mode directly and robustly. A simple DOM API was introduced in IE8 that provides exactly this information: document.documentMode. Modifying the original code to make use of this API results in the following:
// Better, but still don't use
// Avoid in favor of feature detection
if(document.documentMode < 9) {
// Enable styling of new HTML5 elements
...
}
This is better, but the best way to achieve the original goal is detecting directly if styling of HTML5 elements is available, and avoiding browser sniffing altogether.
Here’s how to do it:
// DO THIS: Feature detection for styling unknown elements
var elm = document.createElement("div");
elm.innerHTML = "<foo> test</foo> ";
if(elm.childNodes.length !== 1) {
// Enable styling of new HTML5 elements
var elms = [
"abbr","article","aside","audio","canvas","command",
"datalist","details","figcaption","figure","footer",
"header","hgroup","mark","meter","nav","output",
"progress","section","summary","time","video"
];
for(var i = 0; i < elms.length; i++) {
document.createElement(elms[i]);
}
}
Here’s how it works.
New HTML5 elements can’t be styled by default in versions of IE prior to IE9 for two reasons. The first is because new HTML5 elements are treated as unknown elements. The second is that earlier versions of IE collapse all unknown elements at parse time. Being collapsed simply means that all children of an element actually become children of that element’s parent. Furthermore, collapsed elements end up with separate entries in the DOM for their start and end tags. The following sample code and resulting DOM representations illustrate this point:
var elm = document.createElement("div");
elm.innerHTML = "<foo>test</foo>";
Resulting DOM in IE8
- <DIV>
- test
- </FOO>
Resulting DOM in IE9
- <div>
- <foo>
- test
As you can see, the statement elm.innerHTML = "<foo>test</foo>"
actually results in two children instead of one in browsers that collapse unknown elements. This behavior can be easily identified using feature detection (e.g. testing if elm.childNodes.length !== 1
).
The final piece is understanding that enabling the styling of an unknown element is as simple as calling document.createElement("unknownElementName")
from script before any element of that type is encountered by the HTML parser. Thus enabling the styling of new HTML5 elements involves calling document.createElement
for each new element defined by the HTML5 spec.
This approach is an excellent example of the benefits of using feature detection instead of browser sniffing. Using the above code eliminates worrying about which version of which browser supports styling new HTML5 elements. Since the code tests for the behavior itself, it will automatically apply the appropriate workaround if and only if the workaround is actually needed.
Tony Ross
Program Manager
Edit 2:28pm: edit in the third code sample.
Edit 6/11 – correction in IE8 resulting DOM description.