IE Change Event Delay

The Web is ever changing, and this article is relatively ancient having been published 12 years ago. It is likely out of date or even blatantly incorrect in relation to modern better practices, so proceed at your own risk.

I recently developed a sign-up form for a client that includes on-page price total calculation using JavaScript (jQuery). The premise is simple: the user provides information and specify options, then clicks a radio button to choose a specific price plan. The initial total price calculation is triggered by the change() event for the radio button elements. But, the client was concerned (and with user testing, it turned out rightly so) because in IE, the price calculation didn’t happen until the user clicked somewhere else on the page. In cases where they first clicked one option, then a different one, the price would seemingly lag behind because of IE’s delayed change event firing. It was confusing to the user, but worse—confusing for me to “fix” IE’s implementation.

The awesome news is that this has been fixed in jQuery 1.4, but the concern is still valid for older versions (which my application was using) and straight-JavaScript implementations.

The problem

In Internet Explorer, the change event fires when focus leaves the form element (an event know as blur). That means that the event happens only once a user has clicked on—or used the keyboard to navigate to—another element on the page (form or other).  In cases like mine, where a user is expecting instant feedback to their click, this causes issues with user experience. Unfortunately, this isn’t exactly a “bug,” as it’s how IE handles this event in 6, 7 and 8.

In other browsers—Firefox, Webkit-based (Safari/Chrome) and Opera—the event fires off immediately, so in order to have consistent, intelligent operation, we have to hack IE’s basic behavior. The easiest solution is to bind your function to a different event, such as the click event, but that’s generally not the right solution. There is a better one.

Why using the click event is wrong

One word: accessibility. Users—whether they have a disability that restricts their use of the mouse or like to tab about the page with the keyboard for speed—don’t always use the mouse to move from form element to form element. So, if you bind your functionality to the click event, you may end up messing with a user’s workflow, which makes for unhappy visitors. In some cases, it may even make a user unable to use your application. So don’t use that as your solution.

The real solution

If IE needs a blur event to know that the change event should fire, give it one when the element is clicked. In jQuery, that looks something like:

$("#element").click(function(){
	// In IE, we need to do blur then focus to trigger a change event
	if ($.browser.msie) {
		this.blur();
		this.focus();
	}
}).change(function(){ actionIWantOnChange(); });

This code tricks IE into thinking that focus has been changed away from the element when it is clicked. But since the change event is also triggered, the actions attached to that event also happen. Keyboard navigability still works, because even though there is no click, the change event will fire when they use the keyboard to move to another field meaning the feedback is instantaneous.

Now, you can probably improve my above example by using better feature-sniffing to test for IE instead of the browser object in jQuery, but my time for creating a fix was limited—and this code gets the job done.

The TITLE element and jQuery’s text() function

You’re doing it wrong. In a day of zero fun, a coworker and I ended up tasked with debugging a jQuery-based script that seemed to be perfectly fine, except of course, the part where it wasn’t working as expected in IE. After some tracing, it turns out the issue had everything to do with one line of code not returning a value: $("title").text();. To translate, the author of the code had been trying to retrieve the text of the title element using jQuery’s text function. But, turns out that doesn’t work in IE.

The solution

If you want to retrieve the value of the page title, use document.title. If you want to set the value of the page title—the title displayed at the top of the browser—use document.title.

It doesn’t matter if you’re working with straight JavaScript or a library like jQuery, the correct way to interact with the page title is through document.title.

Here’s a full code example:

var foo = document.title;
alert("The title was "+foo);
// on this site, the above alert would read
// "The title was The TITLE element and jQuery’s text() function"
document.title = "Now it’s been changed.";
alert(document.title);
// this alert = "Now it’s been changed"
// the display at the top of the browser changes too

The explanation

Don’t worry, I’m not going to leave you high and dry without explaining why the original approach didn’t work.

The problem exists in IE because IE doesn’t consider the title element to have child nodes. By W3C specs, the title element contains one, and only one child node: a text node. It can not have any child elements. Just the one text node.  But according to IE, that text exists in some strange nebulous state outside of a normal text node. Here’s a code example.

var titles = document.getElementsByTagName("title");
var kids = titles[0].childNodes;
alert(kids.length);

If you run that code in IE, the alert will be 0. In FireFox, Chrome, Safari, Opera, etc, it will return 1, the expected amount.

Breaking down jQuery.text()

So, the reason $("title").text() doesn’t work in IE is because of jQuery.text()’s reliance on children nodes. Let’s take a look at the function (jQuery version 1.4.2, line 3418):

function getText( elems ) {
	var ret = "", elem;
	for ( var i = 0; elems[i]; i++ ) {
		elem = elems[i];
		// Get the text from text nodes and CDATA nodes
		if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
			ret += elem.nodeValue;
		// Traverse everything else, except comment nodes
		} else if ( elem.nodeType !== 8 ) {
			ret += getText( elem.childNodes );
		}
	}
	return ret;
}

A quick translation of the code is as follows. The first line of the function sets up a variable ret as an empty string. This variable is the return variable for the function. Then we jump into a loop that goes through each object in the array that was passed in (remember, $("title") will return an array with a single element). For each of these elements it checks whether it is a text node. If it is, it appends the node’s value to the return value (ret). If it is an element, it recursively calls itself on that element, appending the result of the recursive call to the return value. At the end, ret is returned.

But, remember, in IE, the title element has no text node, because there are no child nodes. So what does ret equal? An empty string. The entire for loop is skipped, because elems is empty. It essentially goes straight from declaring ret="" to returning that initial value.

The code continues to work in other browsers because they rightly treat the element as having a single child node—a text node— and that node’s value—the text—is added to ret.

innerText doesn’t work either

Even if you’re using plain JavaScript without a a library, document.title is the way to do this. Typically, to set the text of an element in IE you would use the innerText property, but you shouldn’t do that in this situation. There are a couple reasons for this:

  1. It doesn’t work in IE. In fact, it’s a documented issue dating back to IE 5, maybe earlier. At this point, we have to assume Microsoft has it working as they intend it to.
  2. The innerText property doesn’t work cross browser. In FireFox, you have to use the textContent property. Why bother writing extra code to be cross-browser compatible when document.title will work without issue?

Wrapping it up

Here’s that first script again, showing the correct way to do it:

var foo = document.title;
alert("The title was "+foo);
document.title = "Now it’s been changed.";
alert(document.title);

So now you know why you can’t use jQuery.text() to retrieve the page title. But that’s ok, because doing so would be inefficient anyway when you can just call document.title. Good luck with your dev.

Losing Values When Cloning Forms

The Web is ever changing, and this article is relatively ancient having been published 13 years ago. It is likely out of date or even blatantly incorrect in relation to modern better practices, so proceed at your own risk.

I’ve finally started development of a book recommendation widget for the musings and reviews on books I read section of my site. The general functionality is pretty simple: visitors have a few fields to complete with info about the book; upon submission, their recommendation is saved to a database; the new recommendation is shown to all and sundry in a “last recommendation” section; rinse & repeat. The whole no-JS needed, server-side scripting processing involved is simple, straight-forward and was quickly completed. Being a front-end developer, however, I want to make sure this can all be done in a smooth JS-enhanced way as well (for some nifty UX). That’s where I encountered yet another annoying JavaScript problem.

Each browser interprets “clone” differently

Due to differences in how each browser implemented cloneNode() in their JavaScript engines, there is an issue with values for form elements not persisting to the copy of an element. Inconsistencies like this are many of the reasons why I’m a fan of using a JavaScript library for most projects. In this case, because the library has usually worked out the issues and can handle copying without losing important data. Unfortunately, jQuery still has some issues to work out; it loses select and textarea values on clone(). Other inputs types don’t seem to have any issues, including hidden fields.

How this affected my widget

In order to display the submitted form data in the “last recommendation” section, I decided the best approach would be to copy the form, then replace each form element with an inline element containing the value based on type of element. It might not be the most elegant solution, but it seemed better than specifying each individual element by name and manipulating it that way.

So, step one: use clone() to copy the form (no need for cloning events and the like). Step two: do the replacement. Step three: realize that regardless of selection, the last recommendation always displays the first option in a select box. And an empty string for any text area.

Solutions

Honestly, my current solution is just a dirty hack. I only have two affected fields, so I explicitly copy the original values to where they need to go. I’m more concerned about getting this up and running, knowing that there won’t be much in the way of extension in the future. I am about 99% positive I won’t be adding any additional fields, at least.

For other projects though, this could be a major issue for scalability and ongoing maintenance, especially if there are multiple affected elements. A quick search around the Internet shows a lot of inelegant solutions. One that approaches a decent solution for jQuery is this solution at mddev.co.uk, although it only attacks the select issue (and later inputs as well with a bit overkill [see the comments], but no textarea). That approach could work with some modifications.

Have you seen this before? How’d you solve it/work around it?