Prototype Basics
JavaScript libraries don’t start out as libraries. They start out as “helper” scripts.
It’s possible, but impractical, to do DOM scripting without the support of a library.
Little things will start to annoy you from day one. You’ll get tired of typing out
document.getElementById, so you’ll write an alias. Then you’ll notice that Internet Explorer
and Firefox have different event systems, so you’ll write a wrapper function for adding
events. Then you’ll become irritated with a specific oversight of the language itself, so
you’ll use JavaScript’s extensibility to write some code to correct it.
Then one day you’ll notice your “helper script” is 35 KB. When organized and divided
into sections, you’ll realize you’ve got a library on your hands. All JavaScript libraries start
out as caulk between the cracks.
For this reason, a lesson in using Prototype begins not with an in-depth look at any
particular portion, but rather with a whirlwind tour of many of the problem-solving con-
structs you’ll use most often. In this chapter we’ll take that tour.
Getting Started
Let’s keep using the web page we wrote in the previous chapter. Open up index.html and
add some content to the page’s body:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
" /><html xmlns=" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script type="text/javascript" src="prototype.js"></script>
<title>Blank Page</title>
</head>
17
CHAPTER 2
<body>
<h1>Blank Page</h1>
<ul id="menu">
<li id="nav_home" class="current"><a href="/">Home</a></li>
<li id="nav_archives"><a href="/archives">Archives</a></li>
<li id="nav_contact"><a href="/contact">Contact Me</a></li>
<li id="nav_google"><a href=""
rel="external">Google</a></li>
</ul>
</body>
</html>
We’re adding an unordered list with links as list items—the typical structure for a web
site’s navigation menu. Each
li element has an ID. The final a element has a rel attribute
with a value of
external, since it links to an external site. This convention will be quite
useful later on.
We’ll add more markup to this page over the course of the chapter, but this is enough
for now. Nearly all of the code examples from this chapter can be used on this page with
the Firebug shell, so feel free to follow along.
The $ Function
DOM methods have intentionally verbose names for clarity’s sake, but all that typing
gets tiresome very quickly. Prototype’s solution is simple but powerful: it aliases the oft-
used DOM method
document.getElementById to a function simply named $. For example,
instead of
document.getElementById('menu'); //-> <ul id="menu">
with Prototype, you can write
$('menu'); //-> <ul id="menu">
Like document.getElementById, $ finds the element on the page that has the given id
attribute.
Why is it called
$? Because you’ll use it often enough that it needs to have a short
name. It will be the function you’ll use most often when scripting with Prototype. In
addition, the dollar sign is a valid character in object names and has little chance of
having the same name as another function on the page.
It’s far more than a simple alias, though. There are several things you can do with
$
that you can’t do with document.getElementById.
CHAPTER 2 ■ PROTOTYPE BASICS18
$ Can Take Either Strings or Nodes
If the argument passed to $ is a string, it will look for an element in the document whose
ID matches the string. If it’s passed an existing node reference, though, it will return that
same node. In other words,
$ lets you deal with string ID pointers and DOM node refer-
ences nearly identically. For example
var menuElement = document.getElementById('menu');
Element.remove(menuElement);
// can also be written as
Element.remove('menu');
Either way, the element is removed from the page. Why? Listing 2-1 shows how
Element.remove is defined in the Prototype source code.
Listing 2-1. Prototype Source Code
Element.remove = function(element) {
element = $(element);
element.parentNode.removeChild(element);
return element;
};
The highlighted line is used quite often in Prototype. If the element argument is a
string,
$ converts it to a DOM node; if it’s a node already, then it just gets passed back.
It makes code very flexible at a very small cost.
All of Prototype’s own DOM manipulation methods use
$ internally. So any argu-
ment in any Prototype method that expects a DOM node can receive either a node
reference or a string reference to the node’s ID.
$ Can Take Multiple Arguments
Normally, $ returns a DOM node, just like document.getElementById. It returns just one
node because an ID is supposed to be unique on a page, so if the browser’s DOM
engine finds an element with the given ID, it can assume that’s the only such element
on the page.
That’s why
document.getElementById can take only one argument. But $ can take any
number of arguments. If passed just one, it will return a node as expected; but if passed
more than one, it will return an array of nodes.
CHAPTER 2 ■ PROTOTYPE BASICS 19
var navItems = [document.getElementById('nav_home'),
document.getElementById('nav_archives'),
document.getElementById('nav_contact')];
//-> [<li id="nav_home">, <li id="nav_archives">, <li id="nav_contact">]
var navItems = $('nav_home', 'nav_archives', 'nav_contact');
//-> [<li id="nav_home">, <li id="nav_archives">, <li id="nav_contact">]
Prototype’s constructs for working with DOM collections represent a large portion of
its power. In Chapter 3, you’ll learn about how useful this can be.
$ Enhances DOM Nodes with Useful Stuff
Core JavaScript data types can be augmented with user-defined functions. All JavaScript
objects, even built-ins, have a
prototype property that contains methods that should be
shared by all instances of that object. For instance, Prototype defines a method for strip-
ping leading and trailing whitespace from strings:
String.prototype.strip = function() {
return this.replace(/^\s+/, '').replace(/\s+$/, '');
};
" Lorem ipsum dolor sit amet ".strip();
//-> "Lorem ipsum dolor sit amet"
All strings get this method because they’re all instances of the String object.
In an ideal world, it would be this easy to assign user-defined functions to DOM
objects:
HTMLElement.prototype.hide = function() {
this.style.display = 'none';
};
This example works in Firefox and Opera, but fails in some versions of Safari and
all versions of Internet Explorer. There’s a gray area here: the DOM API is designed to
be language independent, with its JavaScript version just one of many possible imple-
mentations. So some browsers don’t treat DOM objects like
HTMLElement the same way
as built-ins like
String and Array. To get this sort of thing to work across browsers
requires a bit of voodoo.
Prototype takes care of this behind the scenes by defining custom element meth-
ods on
HTMLElement.prototype in browsers that support it, and copying these instance
CHAPTER 2 ■ PROTOTYPE BASICS20
methods to nodes on demand in browsers that don’t. Any Prototype method that
returns DOM nodes will “extend” these nodes with instance methods to enable this
handy syntactic sugar.
Once a node has been extended once, it does not need to be extended again. But to
extend all nodes on page load would be prohibitively costly, so Prototype extends nodes
on an as-needed basis.
Let’s illustrate this in code:
var firstUL = document.getElementsByTagName('ul')[0];
firstUL.hide();
//-> Error: firstUL.hide is not a function
You’ll get this error in Internet Explorer. A node must have been extended by
Prototype before you can be sure it has these instance methods. So there are a few
options here:
• Use the generic version of the method. Every instance method of a DOM node is
also available on the
Element object:
var firstDiv = document.getElementsByTagName('ul')[0];
Element.hide(firstUL);
• Instead of using the native DOM method, use a Prototype method that does the
same thing.
var firstUL = $$('div')[0];
firstUL.hide();
• Extend the node just to be safe, using Element.extend or $.
$(document.getElementsByTagName('ul')[0]).hide();
In other words, $ isn’t just an alias for document.getElementById, it’s also an alias for
Element.extend, the function that adds custom instance methods to DOM nodes. You’ll
learn much more about this in Chapter 6.
Object.extend: Painless Object Merging
The object literal is part of JavaScript’s terseness and expressive power. It allows one to
declare an object with any number of properties very easily.
CHAPTER 2 ■ PROTOTYPE BASICS 21
var data = {
height: "5ft 10in",
weight: "205 lbs",
skin: "white",
hair: "brown",
eyes: "blue"
};
But in JavaScript, it’s possible to add any number of properties to an existing object
at any time. So what happens when we want to extend this object?
if (person.country == "USA") {
data.socialSecurityNumber = "456-78-9012";
data.stateOfResidence = "TX";
data.standardTaxDeduction = true;
data.zipCode = 78701;
}
We can’t define new properties en masse—we have to define them one by one. It gets
even more frustrating when extending built-in classes, as Prototype does:
String.prototype.strip = function() {
//
};
String.prototype.gsub = function() {
//
};
String.prototype.times = function() {
//
};
String.prototype.toQueryParams = function() {
//
};
This is a direct road to carpal tunnel syndrome. There’s got to be a better way—we
need a function for merging two different objects.
Prototype gives us
Object.extend. It takes two arguments, destination and source,
and both objects, and loops thr
ough all the properties of
source, copying them over to
destination. If the two objects have a property with the same name, then the one on
destination takes precedence.
CHAPTER 2 ■ PROTOTYPE BASICS22