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" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" 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="http://google.com" 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 . 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. 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. gets even more frustrating when extending built-in classes, as Prototype does: String .prototype. strip = function() { // }; String .prototype. gsub = function() { // }; String .prototype. times =