Welcome to another addition of Beating IE into submission!
It's a well know fact that IE's proprietary .attachEvent(..) function fails with regards to a number of W3C recommendations:
- Fails to honor the 'this' keyword when wiring up events to DOM objects.
- Fails to enforce just one function wired to a given event for a given object.
- Fails to name events appropriately, that is: without prefixing event names with the word 'on'.
- Fails to accept a 3rd parameter, useCapture, which would allow for event capture instead of event bubbling.
In this blog entry, I'm going to look at how to solve the first 3 of these issues (the 4th is more complicated is beyond the scope of this entry).
Before continuing, you may want to review one of my previous entries: The .call(..) and the .apply(..) functions. Understanding these functions will be helpful in understading the techniques we're going to use to fix IE's .attachEvent(..) failures.
Read on to learn more.
First, I'll post the code in its entirety, and then an explanation will follow:
{
evtHash: [],
ieGetUniqueID: function(_elem)
{
if (_elem === window) { return 'theWindow'; }
else if (_elem === document) { return 'theDocument'; }
else { return _elem.uniqueID; }
},
addEvent: function(_elem, _evtName, _fn, _useCapture)
{
if (typeof _elem.addEventListener != 'undefined')
{ _elem.addEventListener(_evtName, _fn, _useCapture); }
else if (typeof _elem.attachEvent != 'undefined')
{
var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) +
'::evt_' + _evtName + '::fn_' + _fn + '}';
var f = xb.evtHash[key];
if (typeof f != 'undefined')
{ return; }
f = function()
{
_fn.call(_elem);
};
xb.evtHash[key] = f;
_elem.attachEvent('on' + _evtName, f);
// attach unload event to the window to clean up possibly IE memory leaks
window.attachEvent('onunload', function()
{
_elem.detachEvent('on' + _evtName, f);
});
key = null;
//f = null; /* DON'T null this out, or we won't be able to detach it */
}
else
{ _elem['on' + _evtName] = _fn; }
},
removeEvent: function(_elem, _evtName, _fn, _useCapture)
{
if (typeof _elem.removeEventListener != 'undefined')
{ _elem.removeEventListener(_evtName, _fn, _useCapture); }
else if (typeof _elem.detachEvent != 'undefined')
{
var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) +
'::evt' + _evtName + '::fn_' + _fn + '}';
var f = xb.evtHash[key];
if (typeof f != 'undefined')
{
_elem.detachEvent('on' + _evtName, f);
delete xb.evtHash[key];
}
key = null;
//f = null; /* DON'T null this out, or we won't be able to detach it */
}
}
};
We start by wrapping everything in an xb (which is my way of abbreviating cross-browser) variable for convenience. This is just so the code we're about to write is confined to some scope and not floating out there in the global space.
We then define a empty array called, evtHash, which, when it is finally populated, will serve as an associative array to keep track of the events we've wired up. This will help ensure that only one function is wired to a given event for a given object.
Next, we define a function ieGetUniqueID(..) which will give us back a unique ID for any element. We need this in order to generate unique keys for the evtHash. If you're wondering why we can't simply use IE's .uniqueID property directly, it's because it returns undefined for the document and for the window objects.
We also define the function signature for two functions: xb.addEvent(..) and xb.removeEvent(..).
{
evtHash: [],
addEvent: function(_elem, _evtName, _fn, _useCapture)
{
…
},
removeEvent: function(_elem, _evtName, _fn, _useCapture)
{
…
}
}
Next, we use object detection to determine if .addEventListener(..) / .removeEventListener(..) are available for the element that was passed into the function. If so, we'll go ahead and use them since they don't suffer from the problems that IE's .attachEvent suffer from.
{
if (typeof _elem.addEventListener != 'undefined')
{ _elem.addEventListener(_evtName, _fn, _useCapture); }
…
},
removeEvent: function(_elem, _evtName, _fn, _useCapture)
{
if (typeof _elem.removeEventListener != 'undefined')
{ _elem.removeEventListener(_evtName, _fn, _useCapture); }
…
}
If, however, .addEventListener(..) / .removeEventListener(..) are not available, we check for the availability of .attachEvent(..) / .detachEvent(..) and we assume we're dealing with IE proprietary javascript.
{
…
else if (typeof _elem.attachEvent != 'undefined')
{
…
}
},
removeEvent: function(_elem, _evtName, _fn, _useCapture)
{
…
else if (typeof _elem.detachEvent != 'undefined')
{
…
}
}
Now things start to get a little tricky. Once we've established that we're dealing with an IE event model, we'll need to store the function for the given event in an event hash. This will ensure that if, at any point in the future, an attempt is made to wire up the same function to the same event for the same object, the attempt will be ignored.
Before we can store the function in the hash though, we have to create it. We can't simply use the variable, _fn that was passed into the function because simply using that function won't matain the 'this' keyword reference. Instead we have to create a lambda function and assign it to a variable. The contents of the lambda function will be a call to execute the given function, _fn using the .call(..) function. This technique will make sure that the 'this' keyword is correctly maintained.
Finally, in order to store the function in the event hash, we need a unique key for it. Since any given combination of object/event/function must be unique, the easiest thing to do is to create a key based on those items. In this case, I'm actually concatening the object's .uniqueID (a property that is very IE specific) with the event name, along with a string representation of the whole darn function. The function signature would probably be adequate, but that would require some parsing, and I'm lazy.
With the key created, we can store the function in the hash. Once that's done, we can actually use .attachEvent(..) to wire up the desired event, but we'll use our lambda function ensure the 'this' keyword is maintained.
Next, we'll hook up an unload event to the window which actully detaches the same function we just previously attached (but only when the page unloads). This will help prevent memory leaks in IE.
Finally, we null out the key variable just to be safe.
The .removeEvent(..) function is pretty similar; it just undoes what .addEvent(..) does. Rather than explain it here, I'll simply put comments directly in code snippet.
{
…
else if (typeof _elem.attachEvent != 'undefined')
{
// create the key
var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt_' + _evtName + '::fn_' + _fn + '}';
// see if the event hash already has this key, and if so, ignore the event wire-up request
var f = xb.evtHash[key];
if (typeof f != 'undefined')
{ return; }
// create a lambda function that uses .call(..) to maintain a proper 'this' keyword reference
f = function()
{
_fn.call(_elem);
};
// store the lambda function in the event hash using the key that was created in the beginning
xb.evtHash[key] = f;
// attach the desired event to the element, but execute the lamba fu nction instead of _fn, so that
// the 'this' keyword is correclty maintained
_elem.attachEvent('on' + _evtName, f);
// attach unload event to the window to clean up possibly IE memory leaks
// IMPORTANT: You may want to omit this 'onunload' bit, as it could potentially
// increase memory consumption, especially in cases where you do a lot of
// manual detaching of events during the life of your app — use this technique with care
window.attachEvent('onunload', function()
{
_elem.detachEvent('on' + _evtName, f);
});
// null out the key so the GC knows it can be eaten
key = null;
//f = null; /* DON'T null this out, or we won't be able to detach it */
}
},
removeEvent: function(_elem, _evtName, _fn, _useCapture)
{
…
else if (typeof _elem.detachEvent != 'undefined')
{
// create the key
var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt' + _evtName + '::fn_' + _fn + '}';
// see if the event has already has this key, and if so, store its value in the f variable
var f = xb.evtHash[key];
if (typeof f != 'undefined')
{
// detach the lambda function, f, that was retrieve from the event hash, populated from .addEvent(..)
_elem.detachEvent('on' + _evtName, f);
// delete this key from the event hash
delete xb.evtHash[key];
}
// null out the key so the GC knows it can be eaten
key = null;
//f = null; /* DON'T null this out, or we won't be able to detach it */
}
}
And that's pretty much it! Using the function should be fairly straight forward:
<script type = "text/javascript">
var d = document.getElementById('myDiv');
xb.addEvent(d, 'click', clickFn, false);
function clickFn()
{
alert(this.id + ' was clicked');
}
</script>
<div id = "myDiv">I'm a DIV!<div>
If you try the above code, you should see that IE now honors the 'this' keyword! Also, no matter how many time you wire up the function, clickFn to the click event for myDiv, the function will only execute once.
These two things are huge wins in terms to making IE act more like a standards-compliant browser.
One iddy bitty piece of code that I glossed right over was the last else in the .addEvent(..) function.
{ _elem['on' + _evtName] = _fn; }
This is really just a fallback in case modern event techniques are not available in the browser. You can leave this piece out if you want to, especially considering that browsers that only have support for DOM Level 0 event hook ups are probably not the ones you're targeting for rich javascript-based web 2.0 applications.
Well that wraps up this entry. Hope you enjoy this latest installment of Beating IE into submission.
Comments welcome.
21 Responses
That is brilliant! Do you think I could overwrite "window.attachEvent" to use your implementation instead?
I am writing an AJAX framework that intercepts all post backs and handles them with out-of-band calls.
When I try to make it work together with pages that use DHTML already I run into problems, because the post back also triggers "onunload" handlers and detaches all event handlers anywhere on the page.
So I'm looking for a way to mimick this or at least a way to trigger the "onunload" event.
Any ideas?
Troels:
Thanks for your comments. I'm not completely sure I understand what you're asking though.
Could you either post back with additional comments explaining it further? Or, if you prefer, you could also email me directly.
You are missing a + in the line:
var key = "{FNKEY::obj_" + _elem.uniqueID + "::evt" _evtName + "::fn_" + _fn + "}";
just after "::evt
Other than that, great!
thanks.
Thanks for catching this Kris!
I've corrected the typo and updated the post.
-Steve
Is there a similar uniqueID property for Firefox that allows you to get a unique number/value for each object.
Clay:
Not that I'm aware of. The best I've been able to come up with is a function that attaches an expando property to a DOM element, like:
// assume the existence of some global guidCounter, m_guidCounter, initialized to 0
function getObjectGUID(_obj)
{
if (typeof _obj.guid === 'undefined')
{
_obj.guid = m_guidCounter++;
}
return _obj.guid;
}
Using this function, you can ask an element for its "unique id":
var uniqueID = getObjectGUID(myElem);
If it doesn't have one, one will be created, and if it does have one, it will be returned.
The major disadvantage of this is that you're adding extra data to a DOM element (which may or may not be a big deal to you).
I figured i'd ask, no real sense is creating a function if I can obtain the same result using a property already present in Firefox.
Thanks for putting out these great articles.
Clay
Thank you very much for this great article! IE is knocked out
Nice job stchur. I appreciate the reference very much.
This article is just so great! I've been looking to fix this attachEvent for a few days and I started to be desperate but your solution is neat and just what I was looking for.
Because the keys could be quite big as they contain the full definition of the function for each entry, I decided to make a small modification: instead of using a single array to store all the keys, I create an array for each function which keys are only need to be composed of the unique ID of the object and the event type. See below:
if(typeof _elem.attachEvent != 'undefined')
{
var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt_' + _evtName + '}';
if(typeof _fn.objEvtId == 'undefined')
{
_fn.objEvtId = [];
}
if(typeof _fn.objEvtId[key] == 'undefined')
{
_fn.objEvtId[key] = true;
_elem.attachEvent('on' + _evtName , function()
{
_fn.call(_elem);
});
}
}
The reason I did that is that I am actually working on a tree-view script with events associated to each node of the tree and it could contain hundreds of them.
Anyway, I am so glad I came across your post, Thanks a lot!
I realise that the modification I brought earlier makes it impossible to detach the events as I usually don't bother doing it. But we're working to fix IE's standards and not detaching events in IE could result in memory leaks…
Sparky:
I too eventually became uncomfortable with the potentially large keys, so I actually modified the function as well. Only, the modified version never showed up on my blog. Instead, it became part of my javascript libary: Gimme.
You can view the source for Gimme's .addEvent(..) function, which does everything this function does, but also fixes a number of other IE issues:
Link to Gimme Source
That file is somewhat large; just search for the function: Gimme.ext.addEvent
Works like a charm :). Thanks for writing it.
Thank you so much for this. Currently in a place where I can't use jQuery and this has just saved my Fridat afternoon!
Many thanks! I am finding the same script. Your code is usefull.
Thanks mate for this amazing script. You've really saved my life here, and saved me a lot of future stuffing around.
No problem Scott! Glad it worked out for you.
BTW, I just posted a new entry, which is related to this one (the Ultimate addEvent function). You might want to check it out as it has a number of improvements over the version offered here.
The Ultimate addEvent function
Thank you so much!!!!! Right before commiting suicide
This was just what i needed to get my website up and running. THANK YOU SO MUCH!
@Alec:
Glad it worked for you. Be sure to check out the Ultimate addEvent function too.
[...] if we merged it with the xb.addEventListener(..) function that was described in the blog entry Fixing IE's .attachEvent(..) Failures. For the sake of brevity though, I chose not to do that [...]