Site Navigation

Friday, August 24, 2007

bug 152 - getElementById returns incorrect objects in IE and Opera

Issue: #152
Affects: IE5, IE5.5, IE6, IE7, Opera 8.2, Opera 9.2
Fixed in: Opera 9.50 alpha 1 build 9500
Almost Fixed in: IE8 Beta 1
Fixed in: IE8 Beta 2

MSIE Feedback ID: 333979

Example:
<script type="text/javascript">
var descField = document.getElementById( 'description' );
alert( descField.nodeName );
</script>

The above code works perfectly, in all browsers returning the element with the id "description". Well almost. IE will return the element with that id, but it will also return any element with a name set to "description". At first glance this may not seem such an issue, but consider this; Do you have a meta tag on any of your pages? Do any of them have a name attribute with the value description? If so, you will get a reference to the meta tag, not the form element (or whatever you thought you were getting, as indicated in the spec for getElementById).

When you think for a moment, about where the name attribute is set, this becomes rather scary. Any named anchor, can now conflict with your well defined element ids.

Notes:
So you might ask, why is this broken in Opera? Opera is usually pretty good at supporting the specs! Well, it seems that for maximum compatibility with IE, even though it is implemented wrong, they mimicked the broken behavior.


Known Workarounds: No direct methods, using Option #3 below is suggested.

Workaround Option: 1
If you rely heavily on getElementById, and you suspect that you may have name conflicts, or that user specific content added to a page may cause conflicts you can use getElementsByTagName( tagName ) then iterate over the results and compare the value returned by getAttribute( 'id' ).

Example Workaround Code:
var inputs = document.getElementsByTagName( 'textarea' );
var descField = null;
for(var i=0;i<inputs.length;i++){
if(inputs.item(i).getAttribute( 'id' ) == 'description' ){
descField = inputs.item(i);
break;
}
}




Workaround Option: 2
If you have the ability to globally apply fixes for IE and Opera, the following code will make both of them follow the spec to a 'T', with a very tiny performance hit.

In this example, we redifine the getElementById method, to work as it was intended.
Example Workaround Code:
//use browser sniffing to determine if IE or Opera (ugly, but required)
var isOpera, isIE = false;
if(typeof(window.opera) != 'undefined'){isOpera = true;}
if(!isOpera && navigator.userAgent.indexOf('Internet Explorer')){isIE = true;}

//fix both IE and Opera (adjust when they implement this method properly)
if(isOpera || isIE){
document.nativeGetElementById = document.getElementById;
//redefine it!
document.getElementById = function(id){
var elem = document.nativeGetElementById(id);
if(elem){
//verify it is a valid match!
if(elem.id == id){
//valid match!
return elem;
} else {
//not a valid match!
//the non-standard, document.all array has keys for all name'd, and id'd elements
//start at one, because we know the first match, is wrong!
for(var i=1;i<document.all[id].length;i++){
if(document.all[id][i].id == id){
return document.all[id][i];
}
}
}
}
return null;
};
}



Oh my! this just gets better and better! (after posting and using this I noted (as did J. Max Wilson) that the second workaround, actually exposes another bug in IE!

Third time's a charm (we hope!)

Workaround Option: 3

Same as workaround 2 above, but rather than test elem.id or elem.getAttribute('id') we'll use the much safer elem.attributes collection. Unfortunately if the element you are looking for, in turn has a child element with a name or id attribute, that is set to "id", it will not test the attribute, but rather the child element. (bug 162 - global namespace pollution)

Example Workaround Code:
//use browser sniffing to determine if IE or Opera (ugly, but required)
var isOpera, isIE = false;
if(typeof(window.opera) != 'undefined'){isOpera = true;}
if(!isOpera && navigator.userAgent.indexOf('Internet Explorer')){isIE = true;}

//fix both IE and Opera (adjust when they implement this method properly)
if(isOpera || isIE){
document.nativeGetElementById = document.getElementById;
//redefine it!
document.getElementById = function(id){
var elem = document.nativeGetElementById(id);
if(elem){
//verify it is a valid match!
if(elem.attributes['id'] && elem.attributes['id'].value == id){
//valid match!
return elem;
} else {
//not a valid match!
//the non-standard, document.all array has keys for all name'd, and id'd elements
//start at one, because we know the first match, is wrong!
for(var i=1;i<document.all[id].length;i++){
if(document.all[id][i].attributes['id'] && document.all[id][i].attributes['id'].value == id){
return document.all[id][i];
}
}
}
}
return null;
};
}




Related Issues: (bug 154), (bug 411), (bug 162).