in

ASP.NET Weblogs

Andy Smith's Blog

Page.RegisterStartupScript('Andy', 'MetaBuilders_WebControls_GainKnowledge();');

getting a this. with addEventListener and attachEvent

When writing script for asp.net controls, it's common to want to attach event handlers via script to elements. For some time now, I've been playing with the various ways to do this in order to find the “Best Way”.

My list of things I wanted to acheive were:

  1. I wanted to have “this.” to refer to the element which has caused the event, such as the button being clicked.
  2. I wanted it to be non-destructive. I wanted to make sure that nobody else's handlers where removed when I added mine.
  3. I wanted it to work on v4-era + browsers.

As of last nite around 2am, I'm fairly satisfied that the following method is pretty much the best that's going to happen.

function XBrowserAddHandler(target,eventName,handlerName) { 
  if ( target.addEventListener ) { 
    target.addEventListener(eventName, function(e){target[handlerName](e);}, false);
  } else if ( target.attachEvent ) { 
    target.attachEvent("on" + eventName, function(e){target[handlerName](e);});
  } else { 
    var originalHandler = target["on" + eventName]; 
    if ( originalHandler ) { 
      target["on" + eventName] = function(e){originalHandler(e);target[handlerName](e);}; 
    } else { 
      target["on" + eventName] = target[handlerName]; 
    } 
  } 
}

Here's an example of how it would be used. (The following code assumes that a couple buttons exist on the form, with an appropriate Array-Declaration with their names, and a call to the Init method.

function Demo_Init() { 
  var theForm = document.forms[0]; 
  for( var i = 0; i < Demo_Buttons.length; i++ ) { 
    var theButton = theForm[Demo_Buttons[i]]; 
    theButton.MyMessage = "This Is The Script-Attached Message On " + Demo_Buttons[i]; 
    theButton.ClickHandler = Demo_ClickHandler; 
    XBrowserAddHandler(theButton,"click","ClickHandler"); 
  } 
} 
function Demo_ClickHandler(e) { alert( this.MyMessage ); } 

using the XBrowserAddHandler method, it doesn't matter if the element declares it's own onclick attribute, everything still runs as expected.

As for my requirements: #1 is satisfied by making the handler a method on the element itself. This usually doesn't play well with the addEventListener/attachEvent stuff, so lets look at why it works.

target.addEventListener(eventName, function(e){target[handlerName](e);}, false);

instead of adding the function itself, i actually declare an anonymous function as the real handler, and this function calls the intended handler thru the given “target” argument, preserving “this”. For those of you coming from a SmallTalk or LISP background, you may be able to correct me here, but the feature being used here is called a “Closure”, because the local args “target”, and “handlerName” are actually kept around for the time that the anonymous event handler function is called, long after they go out of the scope of XCrossBrowserAddHandler. It's some pretty neat stuff, and shows how javascript is one of the coolest languages ever. It's OO, it's Functional, it's Procedural. Whatever you need, it can do it.

Comments

 

Jesse Ezell said:

LOL... just did the same thing here two days ago.
October 6, 2003 4:09 PM
 

Kevin Dente said:

Something about this solution just occurred to me. Is there a way to remove an event handler using this scheme (assuming the attachEvent/detachEvent code path)?
October 13, 2003 8:02 PM
 

Andy Smith said:

I don't think so... but that's ok with me. I have yet to ever need to detach an event.
October 13, 2003 8:25 PM
 

Andy Smith said:

as an addendum to my last comment... notice that "ability to remove event handler" isn't on my list'o'requirements.
October 13, 2003 8:26 PM
 

Aaron said:

Andy, I like your idea! I do a lot of ASP.Net control development at my company and can feel your pain on most of this stuff.

But I have some questions with this implementation:

a) If you're going to support old browsers that don't have event listeners, why bother with the event listeners for any browser. Aren't you just creating more code, more code paths, and more possible errors? Personally, I'd rather everything worked as close to the same on every supported platform as possible.

b) I don't like having to specifically assign the function reference to an expando of the element before adding it as a handler. I'd rather do it all in one step.

c) There are known memory leaks in IE with closures that include references to HTML elements. These mostly manifest themeselves in long-running DHTML scenarios, not ASP.Net applications. But I avoid them as a matter of practice now, just in case.

For these reasons, I've modified your original script; you can assign handlers in one step, the code path is essentially the same no matter the browser; and it avoids potential memory leaks a little more than your original.

What think?

[code]
<html>
<head>
<script>
function Demo_Init() {
var theButton = document.getElementsByTagName("BUTTON")[0];
theButton.MyMessage1 = "hello 1";
theButton.MyMessage2 = "hello 2";
XBrowserAddHandler(theButton, "click", Demo_ClickHandler);
XBrowserAddHandler(theButton, "click", function(e) {
alert(this.MyMessage2 + " from " + e.type);
});
}
function Demo_ClickHandler(e) { alert( this.MyMessage1 + " from " + e.type ); }


function XBrowserAddHandler(target,eventName,fnHandler) {
var originalHandler = target["on" + eventName];
if ( originalHandler ) {
// note that we have just created a memory leak in IE because the closures from
// these two anonymous functions reference "target", an HTML element. nuts :(
target["on" + eventName] = function(e){
XBrowserApplyHandler(this, originalHandler, e);
XBrowserApplyHandler(this, fnHandler, e);
};
} else {
target["on" + eventName] = function(e) {
XBrowserApplyHandler(this, fnHandler, e)
};
}
}

function XBrowserApplyHandler(target, fn, e) {
if (!e) e = window.event;
if (!e) alert("Problem with XBrowserApplyHandler: could not find event object.");

if (Function.prototype.call) {
fn.call(target, e);
} else {
target.__XBrowserElementApply = fn;
target.__XBrowserElementApply(e)
}
}
</script>
</head>
<body onload="Demo_Init()">

<button>hello</button>

</body>
</html>
[/code]
November 10, 2003 5:01 PM
 

Andy Smith said:

Aaron:

a) I want to use addEventListener and attachEvent when I can, because it's more defensive against other scripts that might be on the page. The fact that I've done it the right way, doesn't mean that some other 3rd party control/script did it right. They might just over-write your handler with a simple foo.onbar = myFunction, and then your control is broken. The lower-browser code is only there as a last resort, cross-your-fingers-and-hope, situation. I guess I thought this goal was kind of implied by the use of addEventListener/attachEvent

b) the "make the handler a property" thing is something I've done for a long time, because I've found that often I also want to call these handlers as member functions, usually from some other handler, and it gives me a single model.

c) I am and was aware of this, but I decided that, well, it's not that big of a deal. The memory is reclaimed as soon as a navigation occurs.
November 10, 2003 5:44 PM
 

Aaron said:

a) that makes sense.
b) that's an interesting approach, it never really occured to me to call handlers directly. I can see where it would be useful.
c) I suppose so.

All good points. I'll crawl back into my hole now :)
November 10, 2003 11:22 PM
 

k` said:

I Dunno If you managed to finish it (i'm sure you did), but since I've made had that same idea this summer.

>> 1. I wanted to have “this.” to refer to
>> the element which has caused the event,
>> such as the button being clicked.

function.apply is the way

Function.prototype.concat= function(groupe, params)
{ var retour= new Function();
var tampon= '';
for (var i=0 ; i<groupe.length ; ++i)
{ if (groupe[i])
{ tampon+= 'groupe['+ i +'].apply(this, arguments);'; }
}
eval('retour= function('+ params +') {'+ tampon +'}');
return retour;
}

here's a simple test:

function fonction1(a)
{ alert('1 '+ this.id +' '+ a); }
function fonction2(a)
{ alert('2 '+ this.id +' '+ a); }

function testConcat(id)
{ this.id= id;
this.method1= Function.concat([fonction1, fonction2], 'a');
this.method2= function(a) { fonction1(a); fonction2(a); }
}

var bidule= new testConcat('un');
bidule.method1('ok'); // correct context
bidule.method2('ok'); // 'this.id' undefined

Now let's put that together

implementEvent= function(target)
{ if (target && !(target.addEventListener && target.removeEventListener))
{// array of event handlers for that element
// ex 'mouseover'=> _myHandlers['mouseoverList']
target._myHandlers= new Array();

target.addEventListener= function(type, handler, capture)
{ var lstref= type +'List';
if ('undefined' == typeof(this._myHandlers[lstref]))
{ this._myHandlers[lstref]= new Array();
// check if there is already a handler
if (this['on'+ type])
{ this._myHandlers[lstref][0]= this['on'+ type]; }
// ns4 syntax - code may be improved
if (this.captureEvents)
{ eval('this.captureEvents(Event.'+ type.toUpperCase() +')'); }
}// if (!this._myHandlers[lstref])
// check if handler was previously added
for (var i=this._myHandlers[lstref].length ; i-->0 ; )
{ if (this._myHandlers[lstref][i] == handler) { return; }}
this._myHandlers[lstref][ this._myHandlers[lstref].length ]= handler;
this['on'+ type]= Function.concat(this._myHandlers(lstref), 'e');
}
target.removeEventListener= function(type, handler, capture)
{ var groupe= this._myHandlers[type +'List'];
if (groupe)
{ for (var i=groupe.length ; i-->0 ;)
{ if (groupe[i]==handler) { delete groupe[i]; }}}
this['on'+ type]= Function.concat(this._myHandlers(lstref), 'e');
}
}// if (!(target.addEventListener && target.removeEventListener))

and then:

implementEvent(document);
implementEvent(window);

if (document.layers || window.opera)
{ window.addEventListener('resize', function() { this.location.reload(); }, false); }

I hope this time I won't get bashed...

k` - agggka@hotmail.com
December 6, 2003 10:03 PM
 

k` said:

eek, no indent
I forget, of course it works for other elements

// same as document.getElementByID,
// but with ns4 support
var dummy= Script.getElementById('dummy');
implementEvent(dummy);

dummy.addEventListener(....)
December 6, 2003 10:08 PM
 

Gesti??n de eventos en Javascript - aNieto2K said:

October 16, 2006 10:10 AM

Leave a Comment

(required)  
(optional)
(required)  
Add