Adding DOM Event Handlers to the View
by Jason S. Kerchner on May 29, 2009 (filed in Development)
So, I have some functions that will add DOM elements to the page, allowing me to build a View. Now, to allow the user to interact with the View, I need to add event handlers to the DOM elements.
There are basically three ways of doing this, one for W3C-compliant browsers, one for Internet Explorer, and one using browser-independent JavaScript. Personally, I prefer to use the lowest-common denominator when writing JavaScript. If I can avoid if blocks that cycle through browser-specific code, I’ll be much happier.
Browsers that follow the W3C standards (such as Firefox) use the addEventListener function to attach handlers to DOM elements. IE uses the attachEvent function instead. There are some differences between these two, but both allow for multiple handlers to be attached to a single event through multiple calls to the browser-appropriate function. The IE version, however, has one serious drawback: the this variable in the handler function always refers to the window object, rather than the DOM element that the handler is attached to like the other attachment mechanisms do. So while I could easily use browser detection to determine which function to use to attach handlers, I would still be stuck with the limitations of IE when that version was used.
The browser-agnostic version is to simply attach a handler function to the appropriate JavaScript event property directly. For example,
1 2 3 4 | var btn = document.getElementById('submitButton'); btn.onclick = function() { alert('I have been clicked!'); }; |
The major drawback to this method is that you can only attach one function to the event. This is a limitation easily gotten around, however, unlike IE’s inability to give us a useful this variable in the handler. The benefit to this method is that it works on all browsers (even IE will give us a correct this variable inside the handler if we use this mechanism).
Now another drawback to event handling in general is the fact that the this variable will always point to the DOM element that the handler is attached to. Yeah, I know I said it’s a drawback when it doesn’t point to the element, but while it is useful to know this information, it does make event handling a bit misleading when using object-oriented programming. Consider the following:
1 2 3 4 5 6 7 8 | var OkButton = { okClicked: false, handleClick: function() { this.okClicked = true; } }; document.getElementById('ok-button').onclick = OkButton.handleClick; |
Here I’m declaring a new object called OkButton (line 1). It has a property called okClicked (line 2) which should be set to true when the OK button is clicked. There is a handleClick function (line 3) which will be executed when the OK button is clicked, and will set the okClicked property to true (line 4). Line 8 attaches the handleClick function to the OK button.
Look carefully at line 4. What does the this variable refer to? Intuitively you would expect that it refers to the OkButton object, right? After all, that is typically what we use this for. But in this case, because the handleClick function is being executed from an event, the this variable will refer to the DOM element that the handler is attached to, not to the object that the handler function belongs to. This, to me, is a bit misleading and confusing. To make matters worse, if you call OkButton.handleClick directly, then the this variable will point to the OkButton object (since it was not executed from an event). Ugh, I hate ambiguity.
I would like to change all that, so that I can write JavaScript that is more intuitive and less ambiguous. I want the this variable to always refer to the object, not the DOM element, though I don’t want to lose the reference to the DOM element since it will be useful to have. I also want to be able to attach multiple handlers to a single event.
I have two functions to make that happen. One to attach an event to a DOM element, and another to dispatch an event to one or more event handlers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | LivingMachines.View = LivingMachines.Class({ /* ... Lots of code excluded here for brevity ... */ addDomHandler: function(elem, event, fn) { var ev = '_'+event; // Add underscore to event name (e.g. _onmousedown, _onkeypress, _onclick) if (!elem[ev]) elem[ev] = [ ]; elem[ev].push(scopedFn(this, fn)); elem[event] = this.dispatchDomEvent; }, dispatchDomEvent: function(ev) { // "ev" is the event object // "this" is the element that this event handler is attached to // "elem" is the element that initiated the event (will be a child of "this", or might be "this") ev = ev || window.event; // IE uses global window.event var elem = ev.target || ev.srcElement; // IE uses ev.srcElement var handlers = this['_on'+ev.type]; // e.g. ev.type = mousedown, keyup, click, etc. var result = true; for (var i = 0, len = handlers.length; i < len; i++) { result = handlers[i](this, elem, ev) && result; } return result; } }); |
Obviously, these functions are part of the View class (line 1).
The first function is the addDomHandler method (line 5). This is used to attach an event handler function to a DOM element event. It takes three arguments. elem is the DOM element that we want to attach the event to. event is the name of the event (onclick, onmousedown, etc). These event names must match to DOM element events, so they must be in all lowercase letters. And fn is the handler function to be executed. If you want to add more than one handler to an event, you will need to call addDomHandler once for each handler to be added.
When the addDomHandler function is executed, it first adds an underscore to the event name (line7). So “onclick” becomes “_onclick”, for example. It then checks to see if the underscored name is a property of the DOM element (line 8). If it isn’t it creates the property as an empty array (line 9). This is the property that the handlers will be added to. This is what allows multiple handlers on a single event.
Next, the event handler function is pushed onto the handlers array property that was just created (line 10). The scopedFn function is part of the JavaScript Extensions I created several months ago. Basically, it ensures that a function is always executed in the scope of a given object. In other words, it ensures that the this variable in a function always points to a certain object. In this case, that object is the View (the this argument, which points to the View, is passed in to scopedFn). This is how the this argument in the handler is always guaranteed to reference the View object, and never the DOM element. Don’t worry, we’ll still have access to that DOM element. You’ll see how in a moment.
The final step this function does is to attach an event handler named dispatchDomEvent to the event itself (line 11). Note that we are using the event name that was passed in (not the underscored version). This is the actual event of the DOM element, the one that gets fired when the event occurs.
So at this point, we have a DOM element that has a property (such as “_onclick”, “_onmousedown”, “_onkeypress”, etc.) that is an array of scope-corrected handler functions. The DOM element’s event property (such as “onclick”, “onmousedown”, “onkeypress”, etc.) will execute the dispatchDomEvent method.
Now let’s take a look at what happens when an event is fired, causing the dispatchDomEvent to be executed. Remember, it will be executed when an event is fired. First, it get the correct event object. In most browsers, an event object is passed directly into the function (line 14). In Internet Explorer, the window object contains the event information. I’m using the OR syntax to get the correct value for ev (line 20). When an OR is executed, if the first expression has a value (is not null, undefined, an empty string or zero) then that value is returned from the OR operation and the second expression is never evaluated. If, however, the first expression evaluates to a “nothing” value, then the second expression is evaluated and returned from the OR operation. This ensures that the ev variable always points to the event object, regardless where it comes from.
Next, it grabs the target or source element (line 21). This is the element that the user interacted with, not necessarily the element that the handler is attached to. Why is that? Because events bubble. For example, we can attach an event handler to a DIV element. If the user clicks on an element within that DIV element, the event will bubble up the DOM hierarchy until it finds an event handler. In this case, the one attached to the DIV. So the handler attached to the DIV will be executed, but the source of the event is the element that the user actually clicked on. Most browsers keep this source element in the target property of the event object. Internet Explorer keeps it in the srcElement property. We use the same OR technique as before to get the correct source element.
Next, the function gets a pointer to the event handlers array that was added to the DOM element in the addDomHandler function (line 22).
Events can return true or false. If they return false, it will prevent the default action from happening for a given element. For example, a form submit button, when clicked, will submit the form unless the JavaScript onclick event handler function returns false. This will allow us to submit a form via AJAX, for example. Because we have multiple event handlers attached to the event, we need to track the value they each return. If any one of them returns false, then the dispatchDomEvent function (which is the function actually attached to the event) should return false as well. The result variable allows tracking of the event handler results (line 23).
Finally, it loops through all the handler functions (line 24). Remember that these are all scope-corrected functions, so when they execute, the this variable in each will point to the View object. However, since the dispatchDomEvent method is executed from an event, the this variable in that function (the one we’re looking at right now) will point to the DOM element that the handler was attached to. This is important as we look at actual execution of the handler functions (line 25). handlers points to the array of event handlers, and i is the looping index used to access each in turn. Since each array element points to a function, we can execute it using parenthesis, just like we do any function: handlers[i]( /* arguments */ ). The this variable is passed in as the first argument to the handler function. Remember, it points to the DOM element that the dispatchDomEvent function is attached to. The next argument passed in is the element that initiated the event. That is, the element that the user actually interacted with. The last argument passed in is the ev (event) object. Note that the function is ANDed with the current result value. If any event handler function returns false, then result will be permanently set to false for the remainder of the loop.
The last thing the function does is return the value of result (line 27).
Looking at how the event handler functions are executed, this means that event handler functions should have the following format:
1 2 3 4 5 6 7 8 9 10 11 | MyView = LivingMachines.Class({ inherits: LivingMachines.View, handlerFn: function(sender, source, ev) { // "sender" is the DOM element that the handler was attached to (the element that sent the event) // "source" is the DOM element that the user interacted with (the source of the event) // "ev" is the event object // "this" points to an instance of MyView } }); |
And there you have it. An event handlers mechanism that allows multiple event handlers to be attached to a single event, corrects the this variable to always point to the View object, and gives us access to the DOM element that the handler is attached to and the DOM element that initiated the event.
January 11th, 2010 on 6:24 pm
Good day, I am facing a really strange problem with IE, which I think
it is caused by the addDomHandler, but I am not sure of this.
We are creating a huge application, and my task is to create a button
with Rounded corners, I created the new Button with basically is a
DecoratorButton, that supports ClickListseners( or the new
ClickHandlers), and MouseListeners and FocusListeners as well.
Everything was perfect until we detect a really strange bug, which I
have try to locate but so far I have’nt had such luck.
What is happening, is that every time I just put my pointer over my
new button, the widget which the button directly interacts
dissapear!!!, this is not for all the widgets though, apparently only
on FlexTable and other widgets as well, also this only happens on IE8,
not in Firefox or Chrome or Safari. And when this happens there is no
problem detected in the log, not even an exception.
Does you have any clue of the possible reason for this?
Thank you very much for your help.
If I set a normal button instead of the new Button, this doesn’t seem
to happen. It happens with all the Panels which I tried to make them
to Handle Click and Mouse events. I am using GWT 1.6.
January 14th, 2010 on 6:21 pm
Unfortunately, I have never used GWT and am not at all familiar with Google’s libraries. Are you interfacing this somehow with the library I’ve created?