When I started writing Object Oriented Javascript, there was one major pitfall: Event handlers. Event handlers are functions that are launched when a certain event occurs inside the browser, say a click on an image.
Most people are familar with the onclick, onload (…) attributes in HTML and a few selected ones even know how to work with addEventListener and attachEvent (see below), but no matter what system you use, there’s one thing that never works and that’s using event handlers in an object oriented application.
Why? Well, for some reason (I wish I knew which one), both the W3C, which is the consortium for web standards and Microsoft (which is a company against standards, but they usually behave the same way) defined event listeners to take exactly one parameter, and that’s the function which is supposed to be called when an event occurs.
Time for sample code. Imagine you have three buttons and want each one to display its number when you click on it. And imagine you don’t want to do this manually, because there could be as many as 1000 buttons. Here’s the HTML part:
<html>
<head>
<title>HelloButton</title>
<script></script>
</head>
<body>
</body>
</html>
OK, now let’s define our “Button” class. A class is nothing more than a blueprint which you can lateron use to create objects that are all constructed according to this blueprint… In Javascript they are really easy to make, you just create a constructor function and that’s pretty much it. The only differences to a normal function are that:
a) A constructor can’t return anything, because it always returns the object it created.
b) You can use “this” to set properties of that object (object, not class: each object has its own properties, for example two objects of the class “person” might have a property “name”. For person1, this could be “Hans”, for person2 “Peter”).
So, to define a button class with a property “buttonTitle” and a HTML element to display the button all we have to do is this:
function button(title){
/* Create an input element and save a reference in the element property */
this.element=document.createElement("input");
/* Set the type of the HTML element to "button" */
this.element.setAttribute("type","button");
/* Set the label of the HTML element to title */
this.element.setAttribute("value", title);
/* Append that element to the HTML body */
document.getElementsByTagName("body")[0].appendChild(this.element);
/* Set the title property to the supplied title */
this.title=title;
}
Nice, huh? OK, one more thing before we actually get to the event handlers. To define a function that can also use “this” (that’s called a method) we have to use a special notation…
classname.prototype.methodname=function(parameters){code}
Looks strange, I know. But it never really changes, so all you’ve got to do is copy paste. Let’s say we want to add a “showTitle” method that uses alert() to display this.title:
button.prototype.showTitle=function(){
alert(this.title);
}
Not that hard, is it? OK, now we’ve got everything working, but we still have to actually create the objects using a simple “for” loop (we do this inside a “main” function which we specify for body.onload, because otherwise the code which tries to append something to the body might run before the body is actually there) , so now our whole document looks like this:
<html>
<head>
<title>HelloButton</title>
<script>
function button(title){
this.element=document.createElement("input");
this.element.setAttribute("type","button");
this.element.setAttribute("value", title);
document.getElementsByTagName("body")[0].appendChild(this.element);
this.title=title;
}
button.prototype.showTitle=function(){
alert(this.title);
}
function main(){
var buttonObjects=new Array();
for(var i=0;i<3;i++){
buttonObjects[i]=new button("Button #"+i);
}
}
</script>
</head>
<body onload="main()">
</body>
</html>
So now we have the button class, the objects and the HTML buttons: Hurray! But nothing happens when we click them, because we haven’t defined any behaviour yet.
So let’s extend our constructor function (“function button(title)…”) a little. You can look at the previous post if you want to use proper eventListeners, but right now we’re going to just use the onclick attribute.
The seemingly logical thing would be to just add a line that sets the onclick attribute to this.showTitle, right?
this.element.onclick=this.showTitle;
Sadly, this doesn’t work… we don’t get this.title! Why? well, when an event handler gets executed, it gets executed in such a way that “this” means the element that sent the event, in the case the HTML “input” element, not our object.
So what do we do? Well, there’s a nasty little method that every function in Javascript has: this is the “call” method and it executes a function so that “this” means the first parameter. So what we want to do would be something like:
this.element.onclick=this.showTitle.call(this);
Looks nice I know, but sadly it still doesn’t work, because that line actually evaluates this.showTitle.call(this) and then sets this.element.onclick to the value it returned.
But if we wrap this inside yet another function, we’re at least getting closer: Now onclick is at least set to a function, eventhough it still doesn’t work, because “this” is still the HTML element.
this.element.onclick=function(){this.showTitle.call(this)};
The trick is now to actually create a function that we can somehow pass “this” to and which will then return a new function which has “this” hardcoded to the right value. The thing we need here is called Javascript closures and you can easily find documents that explain in 100 or more pages what it is and how it works… but we really only need to know how to use it.
First we create a new function outside any object. Let’s call it methodize. This function should take as a parameters a given function and a reference to “this”, we call this “scope”:
function methodize(methodize_func, methodize_scope){
}
As we said before methodize should return a function:
function methodize(methodize_func,methodize_scope){
return (function(){});
}
And what should that function contain? Exactly. The nasty “this.showTitle.call(this)”, but because we designed this for any function, not just “this.showTitle” we use “methodize_func” instead and “methodize_scope” instead of “this”.
function methodize(methodize_func,methodize_scope){
return (function(){methodize_func.call(methodize_scope);});
}
Now, when we run methodize we get a function that when executed calls the given method in the given scope. We can now asign that to onclick
this.element.onclick=methodize(this.showTitle,this);
And that’s it! Now we get the eventListener executed in the correct scope! I know it seems hard and unneccessary, but once you get used to it, it’s really just copy/paste. Here’s the full code again:
<html>
<head>
<title>HelloButton</title>
<script>
function methodize(methodize_func,methodize_scope){
return (function(){methodize_func.call(methodize_scope);});
}
function button(title){
this.element=document.createElement("input");
this.element.setAttribute("type","button");
this.element.setAttribute("value", title);
document.getElementsByTagName("body")[0].appendChild(this.element);
this.title=title;
this.element.onclick=methodize(this.showTitle,this);
}
button.prototype.showTitle=function(){
alert(this.title);
}
function main(){
var buttonObjects=new Array();
for(var i=0;i<3;i++){
buttonObjects[i]=new button("Button #"+i);
}
}
</script>
</head>
<body onload="main()">
</body>
</html>
P.S:
There’s one thing that we could add to our methodize function and that’s passing of the event and any other parameters to the methodize_func.
It’s really not much of a deal
function methodize(methodize_func,methodize_scope){
/* Copy the arguments array, which contains all parameters
to methodize_args, except entries #0 and #1 (methodize_func
,methodize_scope) */
var methodize_args=new Array();
for(var i=2;i<arguments.length;i++) methodize_args.push(arguments[i]);
/* Return a function that takes an event parameter itself
and passes it on to methodize_func, along with methodize_args */
return (function(evt){methodize_func.call(methodize_scope,evt,methodize_args);});
}
And voilĂ : Suddently our target function runs not only in the correct scope, but it also receives the event that triggered its execution as first parameter and whatever you specified after methodize_scope as an array.