Sunday, July 26, 2009

Javascript Bind: Making Me Hate Javascript A Little Less

I've been working on a Javascript intensive section of my web app lately, and it's reminding me of how much I hate Javascript. Everything seems so haphazard and chaotic compared to writing Ruby code. The Prototype effort has made things less painful but it's still not fun.

One area of Javascript in particular that's always given me trouble is dynamically creating elements that have event handlers. Let me give an example. Say you want to generate 5 links, each with a different value.

  <div id="buttons">
<input type="button" value="Generate links" onclick="generateLinks()" />
<script type="text/javascript">
function generateLinks() {
for (var i=0; i < 5; i++) {
var a = document.createElement('a');
a.linkNum = i;
a.onclick = function() { alert("i is " + i + ", linkNum is " + this.linkNum); };
a.href = "javascript:void(0)";
a.innerHTML = "Link " + i;
document.getElementById('buttons').appendChild(a);
}
}
</script>
</div>

When you run the code, the value of the variable i inside the function is always the same. That's because it essentially becomes a global variable. So when the event occurs and the function is run, the current value of i is reported. One hackish way around this is to assign a custom property to the element that you are creating. When the function is run, the keyword "this" refers to the element that the event occurs on, so you can easily access the custom property of the element, which was assigned when the element was created.

However, in more complex Javascript, you may want to access the object that you're generating the event handler code in. Let me give an example:

  <div id="buttons">
<input type="button" value="Generate links" onclick="generateLinks()" />
<script type="text/javascript">
var linkGenerator;
function generateLinks() {
linkGenerator = new Object();
linkGenerator.val = "Hello from link generator";
linkGenerator.showAlert = function(index) {
alert(this.val + " " + index);
};
linkGenerator.generateLink = function(index) {
var a = document.createElement('a');
a.linkNum = index;
a.onclick = function() { this.showAlert(index); };
a.href = "javascript:void(0)";
a.innerHTML = "Link " + index;
document.getElementById('buttons').appendChild(a);
};
for (var i=0; i < 5; i++) {
linkGenerator.generateLink(i);
}
}
</script>
</div>

If you run this code, you'll see an error in Firebug: "this.showAlert is not a function". That's because when the event handler function gets run, "this" does not refer to the linkGenerator object. this refers to the a element. So there is no "this.showAlert" property of the a object.


This is where the Prototype bind function comes to the rescue! Simply add .bind() to the end of any function, and pass the object that you want "this" to be when the function is called. Example:

  <div id="buttons">
<input type="button" value="Generate links" onclick="generateLinks()" />
<script type="text/javascript">
var linkGenerator;
function generateLinks() {
linkGenerator = new Object();
linkGenerator.val = "Hello from link generator";
linkGenerator.showAlert = function(index) {
alert(this.val + " " + index);
};
linkGenerator.generateLink = function(index) {
var a = document.createElement('a');
a.linkNum = index;
a.onclick = function() { this.showAlert(index); }.bind(this);
a.href = "javascript:void(0)";
a.innerHTML = "Link " + index;
document.getElementById('buttons').appendChild(a);
};
for (var i=0; i < 5; i++) {
linkGenerator.generateLink(i);
}
}
</script>
</div>

Now, when you generate the links, the "this" inside of the function will refer to linkGenerator and display the correct thing.

In addition to specifying the context for this, you can also pass other variables in to the function. Remember our first example, where we were using the variable i in the event handler, and it was always showing the last value? Well, guess what, bind can help us here too. Simply pass a list of variables in to bind after the this reference, and these variables will be available in the function, set to the values that they were at the time the function is generated, not run. Example:

  <div id="buttons">
<input type="button" value="Generate links" onclick="generateLinks()" />
<script type="text/javascript">
var linkGenerator;
function generateLinks() {
linkGenerator = new Object();
linkGenerator.val = "Hello from link generator";
linkGenerator.showAlert = function(index) {
alert(this.val + " " + index);
};
linkGenerator.generateLink = function(index) {
var a = document.createElement('a');
a.linkNum = index;
a.onclick = function(myI) { this.showAlert(index); alert("i is " + myI); }.bind(this, i);
a.href = "javascript:void(0)";
a.innerHTML = "Link " + index;
document.getElementById('buttons').appendChild(a);
};
for (var i=0; i < 5; i++) {
linkGenerator.generateLink(i);
}
}
</script>
</div>

If i is not passed in to bind and taken as a parameter to the function, i will always report as 5, like it did in the first example. By passing it in to bind, the value at that time gets preserved. Very useful stuff!

One more thing that bind does. If you want to get the event that triggered the function call, put bindAsEventListener instead of bind after the function. This will always pass event as the first parameter.