Last week, Adrian Reber (known in the Fedora community), posted about why he feels Internet Explorer is important. The tone of the post is pretty snooty, but I find myself often in a similar position. I use Firefox most of the time. At work, where I have to use Windows, I have Internet Explorer available, but it’s pretty rare I fire it up. Firefox is simply the better browser.
But, IE is the more common browser on the web, by a pretty significant margin. Surely, Firefox has made significant progress on the marketshare front, but it’s still definitely a minority share. But it has some great tools for web development. By using a combination of the Web Developer Toolbar, and Firebug I’m able to put out CSS and JavaScript far faster than I would be able to without the tools, particularly when it comes to the JavaScript automation, but these tools are only available on Firefox, and can’t really emulate the issues that are encountered when trying to run with IE.
Yesterday, I published a new backend for the Washington State University Catalog, which is based on the ASP.NET MVC Framework. It’s not 100% bug free, but it works well, and I’m actively monitoring it to fix the issues that arise. However, before I published it yesterday, I had to work through a series of issues to make the site work correctly in Internet Explorer. Below is a description of several of those JavaScript issues, and the resolution.
Form Element Detection
The DOM makes handy references for parts of a Form available based on the name attribute of the input element of the form. For instance the following HTML:
This create a simple form, with two input elements. If you hold a reference to the form (either by getElementById, or document.forms, or whatever), you can reference the sub elements by their names. For instance
var frm = document.getElementById("test_form"); frm.text1.value = "Fill in the Form";
Will place the text “Fill in the Form” into the text1 element of the test_form. Only one problem with this, IE doesn’t add such references for dynamically created input elements. On the General Catalog Academic Calendar, I allow users to select a campus that they’re interested in. This is because we have Calendar data for campuses which do not have full Catalog data prepared yet, so it’s a reasonable compromise. However, to build that option, I dynamically create the campus select box. In Firefox, when I set the name attribute of the input element, and insert the code, it just works. I ran into an issue, however, where the function which performs the JavaScript manipulation of the function was getting called twice. Here is the basic code as I’d originally wrote it:
var OnFormLoad = function() { if (form.AC_Select) { form.removeChild(form.AC_Select); } if (catalog.campus === "General" && !form.CampusSelect) { var cs = document.createElement('select'); cs.setAttribute('name', "CampusSelect"); form.insertBefore(cs, form.YearTermSelector); connect.asyncRequest('GET', catalog.base_url + "/AJAX/ValidCalendarCampuses", { success: function(o) { try { var campuses = YAHOO.lang.JSON.parse(o.responseText); } catch (e) { form.removeChild(cs); return; } var option; for (var i = 0; i < campuses.length; ++i) { option = document.createElement('option'); option.appendChild(document.createTextNode(campuses[i])); option.setAttribute('value', campuses[i]); cs.appendChild(option); } event.addListener(cs, "change", OnYearTermSelector_Change); }, failure: function(o) { form.removeChild(cs); } }, null); } } }; event.addListener(yts, "change", OnYearTermSelector_Change); event.onAvailable(form, OnFormLoad);
This uses the YUI to register the onAvailable event to the form, and call the function, which simply removes the Select button, registers the onChange event for the YearTermSelector box, and creates the Campus select box, before using AJAX to get the contents and fill in the box. Simple enough, right?
Unfortunately, it doesn’t work in IE. Internet Explorer does not add form child elements to the form object when they are created dynamically like this. Not only that, but it doesn’t remove references in the form, when the child element is removed from the form. Which means that form.AC_Select will always evaluate as truthy, because IE never nulls it out, and form.CampusSelect will always be falsy, since IE never initializes it. In order to make this work, I had to create a boolean variable outside the scope of the function, and set it in the function to make sure that it worked. Like this:
var OnFromLoadRan = false; var OnFormLoad = function() { if (!OnFormLoadRan) { form.removeChild(form.AC_Select); if (catalog.campus === "General") { // Build Campus Select box // removed for brevity. } OnFormLoadRan = true; } }; event.onAvailable(form, OnFormLoad);
Since the OnFormLoad function is located within a function, this puts the OnFormLoadRan variable in a function-level closure, which means nothing outside of the function which contains OnFormLoad can touch it. This is kind of hacky, but it works, and one could argue that the lower level of de-reference is more performant. It may be, but the code is less clean, and that’s my big problem with it.
Inserting Options into a Select List
Another UI trick I use pretty heavily on a few of my forms is the creation of a “dummy” record in a select box, that is set as the default, which allows me to take advantage of the “OnChange” event of those select boxes more effectively. The example page I’ll use for this is the Academic Unit selector. This page will create the “** Select an Academic Unit ** option, and append it to the top of the list, setting it as the default option. Now, whenever the OnChange event fires, I can be reasonably sure that the user has selected something valid.
I was initially doing the addition of this special element as follows:
var OnFormLoad = function() { if (form.AU_Select) { form.removeChild(form.AU_Select); form.AU_Select = null; var no_select = document.createElement('Option'); no_select.appendChild(document.createTextNode("** Select an Academic Unit **")); form.AU_ID.add(no_select, form.AU_ID.firstChild); if (unit_info.textContent === '') { form.AU_ID.selectedIndex = 0; } } };
The add function is the one in question. The W3C standard for the DOM declares that the add function takes the option element to add to the select box, and, optionally, the option element that you wish the new element to precede. Generally speaking you’ll either omit the second argument, to append to the end, or you’ll use the firstChild reference as I have to insert at the beginning of the list. Actually, this might be somewhere where IE’s API is a bit more sensible, but it is against the standard and it does require special coding around.
In IE there are two methods to fix this, both surrounding that add function call. You can either use add the way IE declares it, where the second argument is the index of the option you want to insert before, as follows:
try { form.AU_ID.add(no_select, form.AU_ID.firstChild); } catch (ex) { form.AU_ID.add(no_select, 0); }
Or, you can use the more general DOM manipulation method, insertBefore:
form.AU_ID.insertBefore(no_select, form.AU_ID.firstChild);
Which you choose is dependent of which you prefer, and I’m not sure what the best answer is. insertBefore is more direct, but if you’re using insertBefore to insert children, I’d suggest using appendChild to add new elements to the end of the list, for consistencies sake. That’s really the more important part is being consistent.
But there was one more bug in first version of the function that you may or may not have seen.
Default Values for Contents
Yep, when I make that comparison of the unit_info.textContent to the empty string, I am depending on browser specific behavior.
So, to review, the code is this:
if (unit_info.textContent === '') { form.AU_ID.selectedIndex = 0; }
Which works great in Gecko and Webkit-based browsers. However, IE does not default the value of textContent to the empty string for empty divs. I suspect this is related to the fact that IE doesn’t treat white space as their own DOM elements, but the point is that on IE, unit_info.textContent is null, so it does not absolutely equal the empty string, like I’m checking. Plus, even if I allow type coercion by not using the tripe-equals operator, null and the empty string are not equal. Luckily they are both falsy, so changing the if statement to the following works fine:
if (!unit_info.textContent) { form.AU_ID.selectedIndex = 0; }
JavaScript is a poorly defined language, with a lot of implementation level details that we need to be aware of. It would be fantastic to have a more standardized platform, and we’re slowly working our way there, but somehow I doubt we’ll even be truly free of these sorts of niggling implementation details.