Dealing with Internet Explorer in your JavaScript Code

It’s almost the end of 2008, and thanks to the hard work of web standardistas, browser vendors, and JavaScript framework developers, cross-browser JavaScript code is much less of an issue than it used to be. Even Microsoft is feeling the love — the upcoming Internet Explorer 8 will be a (mostly) clean break from legacy Internet Explorer releases and will behave much more like Firefox, Safari (WebKit) and Opera. …And they rejoiced.

So why is it that when I look under the hood of some recently produced web pages (learning management systems, courses produced by e-learning rapid development tools, general web pages, etc.), the pages’ JavaScript often includes incredibly out-of-date and bad-practice Internet Explorer detection? Check out these samples I randomly copied:


_ObjBrowser.prototype.Init = function() {
    var $nBrowserChar    = "";
    var $nBrowserStart   = 0 ;
    if ( this.$strUA.indexOf("MSIE") >= 0 ){
        this.$nBrowser = BROWSER_IE ;
        this.$nBrowserVersion = "";
        $nBrowserStart   = this.$strUA.indexOf("MSIE")+5
        $nBrowserChar    = this.$strUA.charAt($nBrowserStart);
        while ( $nBrowserChar != ";" ){
            if ( ( $nBrowserChar >= '0' && $nBrowserChar <= '9' ) || $nBrowserChar == '.' )
                this.$nBrowserVersion += $nBrowserChar ;
            $nBrowserStart++;
            $nBrowserChar     = this.$strUA.charAt($nBrowserStart);
        };
        this.$nBrowserVersion = parseInt( parseFloat( this.$nBrowserVersion ) * 100 ) ;
    } else if ( this.$strUA.indexOf("Mozilla") >= 0 ){
        this.$nBrowser        = BROWSER_MOZILLA ;
        this.$nBrowserVersion = parseInt ( (this.$strUA.substring( this.$strUA.indexOf("/") + 1,  this.$strUA.indexOf("/") + 5  )) * 100 );
    }
};

or even these simpler yet equally problematic sniffers:

UserAgent detection

if (navigator.appName &&
    navigator.appName.indexOf("Microsoft") != -1 &&
    navigator.userAgent.indexOf("Windows") != -1 &&
    navigator.userAgent.indexOf("Windows 3.1") == -1) {
        //Do something
}

Bad object detection

if (document.all) {
    //Do something for Internet Explorer 4
} else if (document.layers) {
    //Do something for Netscape Navigator 4
} else {
    //Do something for other browsers
}

These examples are BAD BAD BAD. Why? Well, there are a million web articles explaining the topic, but I’ll give you a quick rundown.

You shouldn’t test for specific browsers, but for specific functionality instead

In most cases, browser detection is being used because the developer is making an assumption that a particular browser does or doesn’t have a specific feature. The problem is that browsers change over time, and detecting for a browser based on assumptions can come back to bite you. Even the prickly Internet Explorer gets updates from time to time. Case in point: The versions of IE6 in Windows 2000 and Windows XP have a different JavaScript engine (JScript version) than IE6 running in Windows XP Service Pack 3. This means a general IE6 detection script might not lead you down the path you expected.

If you test for features instead, your code will be more future-compatible and less likely to break.

if(document.getElementById){
   //It is safe to use document.getElementById()
} else {
   //document.getElementbyId() is not supported in this browser
}

When hacks for Internet Explorer are required

Nowadays, most browsers offer roughly the same features and adhere to W3C standards. But, as we know, our friend Internet Explorer is… different. If you must use hacks custom code for Internet Explorer, know your options (and please, for the love of… something… don’t use ActiveX controls. Just don’t.).

Use cleaner IE detection

The old User-Agent sniffing approach has been abused for years. Internet Explorer 7 (Windows XP SP2) uses the following User-Agent to identify itself:

Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)

Yes, you read that correctly: IE declares itself a Mozilla browser. Opera’s User-Agent can be changed on the fly, allowing a user to set it to Opera, IE, or Mozilla! This all renders User-Agent detection useless. So what are your other IE detection options?

Conditional comments. Conditional comments are a proprietary Microsoft mechanism that is often used to include IE-specific CSS files in a page. For our discussion, you could conceivably use conditional comments to include a JavaScript file containing IE hacks in your page. You could also do a quick and dirty hack like this:


<!--[if IE]>
   <script type="text/javascript">
      var isIE = true;
   </script>
<![endif]-->

But, as you can see, it’s a clunky solution and isn’t inline with the other JavaScript, which makes it a less-than-ideal option.

Conditional compilation. This method is THE method of choice right now. Dean Edwards described this gem of an approach on his blog post Sniff. As far as I can tell, it is the cleanest and most reliable way to detect IE.

var isMSIE = /*@cc_on!@*/false;

Simple, isn’t it? No User-Agents, no 20 lines of conditional statements and string searches.

How does this code work? Internet Explorer uses JScript, a proprietary form of JavaScript (ECMAScript) developed by Microsoft. JScript has a feature called conditional compilation, which allows us to add JScript-specific JavaScript inside a specially formatted comment. Other JavaScript compilers will ignore this code, thinking it’s a comment, while JScript will recognize it and process it.

/*@cc_on begins the comment, and @*/ ends the comment. When any non-IE browser sees var isMSIE = /*@cc_on!@*/false;, it actually sees it as var isMSIE = <strong>false</strong>;. Internet Explorer’s JScript will add the exclamation point contained in the comment, meaning IE will see var isMSIE = <strong>!false</strong>;. Remember, an exclamation point reverses a boolean’s value, so !false is equivalent to true.

As Dean pointed out, there is an even simpler way to write this (though it’s slightly less legible):

var isMSIE/*@cc_on=1@*/;

If a variable is not assigned a value when it is declared, by default it will return ‘falsy‘ (null). Thus a non-IE browser will see this line as var isMSIE; while Internet Explorer’s JScript will see it as var isMSIE=1; (assigning a 1 makes the variable ‘truthy‘).

I prefer to shorten it a bit more by removing the MS from isMSIE, making it simply isIE. Note: it’s a best practice to give booleans a name that implies what the boolean stands for, usually in a quasi-question format. Using plain old var IE doesn’t tell us the variable is a boolean, and doesn’t tell us what question is being answered by the boolean’s value. var isIE does.


var isIE/*@cc_on=1@*/;
if(isIE){
   //Do something. I suggest downloading Firefox.  ^_^
}

Avoid forking your code for hacks whenever possible.

If your code can be written in a way that satisfies all browsers, do it that way, even if it veers to the left of official web standards — just be sure to document your decision in the comments. For example, using the standards-friendly setAttribute for specifying class names will fail in IE, while direct assignment will work in all major browsers.

//standards-friendly, but fails in IE
element.setAttribute("class", "myClass");

//direct assignment, works in all major browsers
element.className = "myClass";

In this case, I’m advocating using defacto standards that aren’t codified in the W3C standards, but are supported in every major browser. Some standardistas will balk, but this is a much easier way to maintain code; it’s a bad idea to use unnecessary conditional statements simply to prove that IE doesn’t always follow standards. We know, we don’t like it either, but get over it. If a non-standard but universally supported alternative is available, your extra “see I told you so” code will be pure bloat, increasing file size, bandwidth requirements, and browser processing requirements.

Don’t get me wrong: standards are important. Very important. But they only go so far; if we stuck to codified standards on principle, no one would be using AJAX today! That’s right, xmlhttprequest — the heart of AJAX — was created by Microsoft, then copied by other browsers. It is not an official standard, but it is universally supported, and is used by tens of millions of web site visitors every day.

If you’re not comfortable with non-standard code littered throughout your project, abstract it into a function so you can easily modify it later.

Old way:

element.className = "myClass";

Abstracted function:

function setClass(targetElement, name){
   /*
      not using setAttribute here because setAttribute
      won't work with class names in IE
   */
   targetElement.className = name;
}

setClass("element", "myClass");

Now whenever you need to set a class, you won’t worry about IE and can simply use your class function. If you decide to change how you assign the class name, you can just change it in the function and not have to dig around your code looking for every instance of direct assignment (className =).

Note: Many JavaScript frameworks, such as jQuery and MooTools, provide this type of support function for you. These frameworks protect you from needing to know about browser inconsistencies, and free you to focus on your application. If the above example were rewritten to use MooTools, it would simply be

element.addClass("myClass");

There are many ways to approach the problem, and all without using nasty browser detection scripts. It pays to know your options.

Use abstractions for forking code

Getting back to IE detection, if you aren’t using a framework and need to handle browser inconsistencies on your own, it’s a good idea to use support functions. These can encapsulate the browser inconsistencies and isolate them from the rest of your code. This makes your code more maintainable and readable.

Case in point: Internet Explorer will not allow you to dynamically assign a name value to an <input> element. This requires an IE-specific hack.

You would normally use the following W3C-compliant code:

var input = document.createElement("input");
input.setAttribute("name", "myInput");

In Internet Explorer, we’re forced to do this:

var input = document.createElement("<input name='myInput'>");

In your project, you’d have to use conditional statements each time you need to create an input element.

var isIE/*@cc_on=1@*/;

if(isIE){
   var input1 = document.createElement("<input name='myInput1'>");
} else {
   var input1 = document.createElement("input");
   input.setAttribute("name", "myInput1");
}
input1.setAttribute("id", "myInput1");

//Later on in your code...
if(isIE){
   var input2 = document.createElement("<input name='myInput2'>");
} else {
   var input2 = document.createElement("input");
   input.setAttribute("name", "myInput2");
}
input2.setAttribute("id", "myInput2");

What a pain. The best way to deal with this is to wrap the code in a function like so

function createInput(parentElement, id){

   //Make sure everything is supported. Error-checking is divine.
   if(!parentElement || !id || !document.createElement || !parentElement.createElement){ return false; }

   var isIE/*@cc_on=1@*/;
   var input;

   if(isIE) {
      input = parentElement.createElement("<input name='" +id +"'>");
   } else {
      input = parentElement.createElement("input");
      input.setAttribute("name", id);
   }

   input.setAttribute("id", id);
   return input;

}

As you can see, the code is now easily reusable throughout our project, contains better support for IE detection, and also contains feature detection ensuring our function won’t throw any errors. To use the code in the project, you’d simply write:

var input1 = createInput(document, "myInput1");
var input2 = createInput(document, "myInput2");

In closing

I hope you’ve found this post helpful, and I especially hopes it helps spread the word about using best practices and cleaner code techniques. I’ll be the first to say I’m no programming expert, so feel free to add a comment if you have suggestions for improvements. 🙂

PS: If you have to write a conditional statement, follow Crockford’s advice and wrap it in curly braces.

Advertisements

MooTools 1.2 & Opera 9.5 released, FF3 coming soon

Some quick development news:

  • MooTools 1.2 has finally been released. MooTools is really great, I hope more people start using it.
  • Firefox 3 should be officially released this Tuesday (June 17). Party on.
  • Oh, while I’m at it, I guess I should mention Opera 9.5 was just released, too. I’m not a big Opera fan, but v9.5 includes some important advances in CSS3 support, which hopefully will help other browser developers to make some progress with their own CSS3 implementations.