Autonomous Machine

On Your Best Behavior: Prototype, LowPro, and Responsible Scripting

The Prototype JavaScript library was born out of the aura that was the early days of Ruby on Rails, and it has grown into a respectable framework used on many high profile sites. ProtoType is a general purpose framework, adding convenience methods to many of JavaScript's built in object types. This approach makes it easier to write more concise, explanatory code, but it has also generated legions of Prototype dissenters. I think it's safe to say Prototype is the right library for some developers and situations, and other libraries may be a better fit for others. Plenty of sites are built with Prototype, and plenty more will be; my goal here is to show how to use Prototype the right way.

If you're working on a jQuery-based project, there's a version of LowPro for you too. The rest of this article will be solid theory, but the syntax used in the following examples uses Prototype.

Problems with Prototype

The biggest problem that I've personally had with Prototype is it tends to be slow to adopt best practices emerging in other libraries. For example, it had an anemic event model before version 1.5, and it didn't natively support any kind of DOM-ready event until 1.6. Luckily, there has been another small JavaScript library that has filled in these functionality gaps for quite some time- LowPro. It you are using Prototype to write your client side code, you should install LowPro before you install Script.aculo.us. If you're using Rails, add installing LowPro to your list of new project setup tasks.

In my opinion, LowPro should have been merged into Prototype long ago. And while its code has never made it into Prototype's codebase, over time, LowPro's intentions have been gradually assimilated. As a result, LowPro's role has grown smaller with each release of Prototype, and today it serves one primary purpose: binding behaviors to page elements. It also has some convenience methods for constructing DOM elements, but I won't be dealing with those in this article.

An introduction to twenty-first century scripting

It's 2008. There is a short list of things that should never be seen in markup from this point forward:

  • inline event handlers
  • script elements outside of your document's head

Yes, this includes your Google Analytics tracking scripts. Put those script tags in your document's head where they belong, and use DOM ready events to fire your tracking.

The key to modern scripting is in your markup. Make it as semantic as you can. Use class names that makes sense, more than one if necessary. Use microformats if you can. Once you do this, you will find id attributes become much less important. I tend to only use them for database objects these days- and even then it can get you into trouble.

There are a lot of third party libraries that expect to be passed an element with an id attribute to function properly. I believe parts of Script.aculo.us are like this. Do not allow these libraries seduce you into adding id attributes where you do not otherwise need them. Behavior-based JavaScript is based around the relationship between elements in the DOM. In most cases, none of the elements need id attributes. From inside behaviors, use DOM traversal methods to find other nodes you need to work with.

With that covered, let's move on to some code examples.

LowPro Basics

The core of LowPro revolves around two functions: Event.addBehavior and Behavior.create. Event.addBehavior can map DOM elements to anonymous functions based on CSS selectors:

Event.addBehavior({
  'div.main': function() {
    // 'this' is a reference to a matching DOM node
  },
  'div.secondary,div.tertiary': function() {
    // multiple CSS selectors can be separated by a comma
  }
});

Event.addBehavior also supports attaching event listeners to DOM elements. Note how the CSS selector has a pseudo-class appended to it:

Event.addBehavior({
  'div.main:click': function(event) {
    // 'this' is a reference to a matching DOM node
    // 'event' it the standard Prototype-extended event object
  }
});

Now when a user clicks on a page element that matches the selector 'div.main', the anonymous function is passed the standard Prototype-extended event object and executed, which is cool. This is a good place to start when writing behaviors, since a good portion of the time, a single event handler will do the job. But if you start to see a lot of duplication of selectors or you need to maintain a state information for your various event handlers to use, it's best to move to creating Behavior classes.

Oh, Behave!

Here's what a simple Behavior looks like- it's generally a good idea to define your behaviors first, and then reference them inside an Event.addBehavior block:

var SimpleBehavior = Behavior.create({
  initialize: function() {
    // Called when the behavior is created
    // 'this' is a reference to the Behavior instance
    // 'this.element' is a reference to the matching DOM element
  },
  onclick: function(event) {
    // 'this' and 'this.element' work the same way as in initialize
    // All 'on...' methods are automatically bound as event listeners
    // to their respective events
  },
  otherMethod: function(arg) {
    // You can add other methods to the behavior object
  }
});

Event.addBehavior({
  'div.main': SimpleBehavior
});

Behavior.create works a lot like Prototype's Class.create- both return class objects that support pseudo-inheritance. You can use these classes and the new keyword to instantiate a behavior. Doing so requires the first argument to the constructor be the DOM element to bind the behavior to. This is what Event.addBehavior does behind the scenes.

new SimpleBehavior(element);

Remember that if you want to store data in properties on the Behavior object instance, the correct way to do so is initialize those properties in an initialize function:

var BehaviorWithProperties = Behavior.create({
  initialize: function() {
    this.property = 'value';
  }
});

A common point of confusion is how behaviors and DOM elements interact. LowPro Behavior classes (and the rest of the examples in this article) involve the interaction of two parts: an instance of the Behavior class, and the DOM element this instance is bound to. I find that this is a stumbling point, but it's simple once the you grasp the way these parts interact: the behavior instance has a reference to the DOM element, but the DOM element knows nothing of the behavior. This is in part to get around some circular-reference memory leaks in certain JavaScript interpreters, and partly because this pattern has emerged as a good design pattern to follow when implementing user interfaces.

Pseudo-inheritance, Prototype style.

Behavior.create handles subclassing behavior in the same way that Prototype's Class.create does. By writing basic behaviors and then extending them for your application-specific purposes, you can build up a library of general behaviors that solve common problems. I usually try to identify a generic behavior component when I'm writing a new behavior, and finish that before building any application specific code.

var Parent = Behavior.create({
  initialize: function() {
    this.element.addClassName('crazy');
  }
});

var Child = Behavior.create(Parent, {
  initialize: function($super) {
    $super();
    this.element.hide();
  }
});

DOM Ready and You

Old-school JavaScript taught developers to use inline onload event handlers on a document's body element to call page initialization methods. The trouble with this is we don't really want the page load event, because it is fired once the page is finished loading. For scripting purposes, all we care about is the DOM being fully parsed and assembled. Starting in version 1.6, Prototype has supported custom events, the most vital of which is 'dom:loaded'. This event is fired right when we need it to: once the DOM is ready.

Event.addBehavior hooks into Prototype's 'dom:loaded' event, and waits until then to look for elements to initialize. Remember that if you instantiate behaviors using JavaScript's new keyword, you need to make sure to do it after the DOM has been loaded.

var MyBehavior = Behavior.create({
  initialize: function() {
    this.element.addClassName(Cycle.thru('odd', 'even'));
  }
});

document.observe('dom:loaded', function() {
  $$('ul.products li').each(function(element) {
    new MyBehavior(element);
  }); 
});

Now Go Out and Make The World a Better Place

I've found LowPro to be an essential tool for writing user interfaces over the last year. It's greatest downside has always been its somewhat sparse documentation, which I hope to have somewhat rectified with this article. Be sure to check out Dan Webb's website for more LowPro and other web goodness. And the LowPro Google Group is a great place to go for help and discussion.