What's New in Prototype 1.5?
January 24, 2007
The latest release of Ruby on Rails, version 1.2, was announced last week to great fanfare. But the announcement might have overshadowed news of a simultaneous release: version 1.5 of Prototype, the popular JavaScript library. Despite the synchronization and developer overlap between the two projects, nothing about Prototype depends on Rails—it's perfectly suitable for use with any server-side technology. In fact, Prototype has amassed a huge user base beyond the Rails community—from dozens of Web 2.0 startups to household names like Apple, NBC, and Gucci.
The Prototype library is fairly compact (about 15K), and decidedly not a kitchen-sink library. It doesn't provide custom widgets or elaborate visual effects. Instead, it just strives to make JavaScript more pleasant to work with. In many ways, Prototype acts like the missing standard library for JavaScript—it provides the functionality that arguably ought to be part of the core language.
Despite the minor version number bump, the 1.5 release is a major one. It's been over a year since 1.4 was released, and the library has made significant strides in that time, while retaining complete backward compatibility (with one notable exception, but more on that later). In this article, we'll look at what's new, organized into four major areas: Ajax support, String extensions, Array/Enumerable extensions, and DOM access.
For an introduction to some of Prototype's capabilities, see Prototype: Easing AJAX's Pain and The Power of Prototype.js. To get a feel for the breadth of the library, peruse the new, long-awaited API documentation on the new, long-awaited Prototype website. Or check out my book, Ajax on Rails, which includes an exhaustive reference to Prototype, as well as its companion library script.aculo.us. The Prototype reference is also available as a PDF: Prototype Quick Reference.
Ajax Support
Prototype is perhaps best known for its top-notch Ajax support. Of course, Ajax-style interactions can be created without a JavaScript library, but the process can be fairly verbose and error-prone. Prototype makes Ajax development more accessible by accounting for the varieties of browser implementations and providing a clear, natural API. The 1.5 release adds even more power, especially as relates to creating RESTful, HTTP-embracing requests. Prototype now has the ability to easily access HTTP request and response headers and simulate HTTP methods other than GET and POST by tunneling those requests over POST. Specifically:
-
Query parameters can now be provided as an object-literal to the
parameters
option on any Ajax method. The object is converted into a URL-encoded string. For example:// Requests /search?q=ajax%20tutorials
new Ajax.Request('/search', { parameters:{ q:'ajax tutorials' } }); -
Like query parameters, request headers can also be provided as an object-literal to the
requestHeaders
option. For example:new Ajax.Request('/search', { requestHeaders:{ X-Custom-Header:'value1' } });
-
All requests now include an Accept header, which informs the server of the preferred response format. The default (
text/javascript, text/html, application/xml, text/xml, */*
) can be overridden using therequestHeaders
option. For example:new Ajax.Request('/data', { requestHeaders:{ Accept:'text/plain' } });
-
The
contentType
option sets the Content-Type request header, which defaults toapplication/x-www-form-urlencoded
. Anencoding
option can also be specified, which defaults toUTF-8
. For example:var myXML = "<?xml version='1.0' encoding='utf-8'?>\n<rss version='2.0'>...</rss>"
new Ajax.Request('/feeds', { postBody:myXML, contentType:'application/rss+xml', encoding:'UTF-8' }); -
The standard
XMLHttpRequest
object at the heart of Ajax functionality only allows HTTPGET
andPOST
methods, but RESTfully-designed web applications often call for the lesser-used methods, likePUT
andDELETE
. Until browsers support the full range of HTTP methods, Prototype offers a compromise: "tunneling" those methods overPOST
, by including a_method
query parameter with the request. You can now specify the intended HTTP method with themethod
option on all Ajax functions (the default isPOST
). Methods other thanGET
orPOST
will actually be requested withPOST
, but will have a_method
query parameter appended to the request URL. For example:// Creates a POST request to /feeds/1.rss?_method=PUT
new Ajax.Request('/feeds/1.rss', { method:'put', postBody:myXML, contentType:'application/rss+xml' });Of course, the server side of the application must be written to understand this convention as well, but if you use Rails, you'll get the behavior for free.
String Extensions
In a typical web application, a great deal of code is written to simply manipulate strings. Thus, a thorough set of string-processing methods are an invaluable weapon in the web developer's arsenal. With version 1.5, Prototype's suite of extensions to the standard String class (or more accurately, the String prototype) has roughly doubled. Here are the latest additions.
-
strip()
removes leading and trailing whitespace from a string. For example:" foo ".strip(); // => "foo"
-
gsub(pattern, replacement)
returns the result of replacing all occurrences of pattern (either a string or regular expression) with replacement, which can be a string, a function, or aTemplate
string (see theTemplate
class below). If replacement is a function, it's passed an array of matches. Index zero of the array contains the entire match; subsequent indexes correspond to parenthesized groups in the pattern. For example:"In all things will I obey".gsub("all", "ALL"); // => "In ALL things will I obey"
"In all things will I obey".gsub(/[aeiou]/i, "_"); // => "_n _ll th_ngs w_ll _ _b_y"
"In all things will I obey".gsub(/[aeiou]/i, function(x){ return x[0].toUpperCase(); }); // => "In All thIngs wIll I ObEy"
'Sam Stephenson'.gsub(/(\w+) (\w+)/, '#{2}, #{1}'); // => "Stephenson, Sam" -
sub(pattern, replacement[, count])
is identical togsub()
, but takes an optional third argument specifying the number of matches that will be replaced, defaulting to one. For example:"In all things will I obey".sub(/[aeiou]/i, "_"); // => "_n all things will I obey"
"In all things will I obey".gsub(/[aeiou]/i, "_", 3); // => "_n _ll th_ngs will I obey"
'Sam Stephenson'.sub(/(\w+) (\w+)/, '#{2}, #{1}'); // => "Stephenson, Sam" -
scan(pattern, iterator)
finds all occurrences of pattern and passes each to the function iterator. For example:// Logs each vowel to the console
"Prototype".scan(/[aeiou]/, function(match){ console.log(match); }) -
truncate([length[, truncation]])
trims the string length characters (default is 30) and appends the string truncation, if needed (default is "..."). For example:"Four score and seven years ago our fathers brought".truncate(); // => "Four score and seven years ..."
"Four score and seven years ago our fathers brought".truncate(20); // => "Four score and se..."
"Four score and seven years ago our fathers brought".truncate(30, ' (read more)'); // => "Four score and sev (read more)" -
capitalize()
returns a string with the first character in uppercase. For example:"prototype".capitalize(); // => "Prototype"
-
dasherize()
replaces underscores with dashes. For example:"hello_world".dasherize(); // => "hello-world"
"Hello_World".dasherize(); // => "Hello-World" -
underscore()
replaces"::"
s with"/"
s, convertsCamelCase
tocamel_case
, replaces dashes with underscores, and shifts everything to lowercase. For example:"Foo::Bar".underscore(); // => "foo/bar"
"borderBottomWidth".underscore(); // => "border_bottom_width" -
succ()
returns the"next"
string, allowing forString
ranges. For example:"abcd".succ(); // => "abce"
$R('a','d').map(function(char){ return char; }); // => ['a','b','c','d']
In addition to Prototype's new extensions to the String
prototype, it also
defines an entirely new class for string manipulation: Template
, which provides
simple templating functionality with JavaScript strings. Using the Template
class is simple: just instantiate a new template with the constructor, and then call
evaluate
on the instance, providing the data to be interpolated. For
example:
var row = new
Template('<tr><td>#{name}</td><td>#{age}</td></tr>');
To render a template, call evaluate
on it, passing an object containing the
needed data. For example:
var person = { name: 'Sam', age: 21 };
row.evaluate(person); // =>
'<tr><td>Sam</td><td>21</td></tr>'
row.evaluate({})); // =>
'<tr><td></td><td></td></tr>'
The default template syntax mimics Ruby's style of variable interpolation (e.g.,
#{age}
). To override this behavior, provide a regular expression as the
second argument to the constructor. For example:
// Using a custom pattern mimicking PHP syntax
Template.PhpPattern =
/(^|.|\r|\n)(<\?=\s*\$(.*?)\s*\?>)/;
var row = new
Template('<tr><td><?= $name ?></td><td><?= $age
?></td></tr>', Template.PhpPattern);
row.evaluate({ name: 'Sam', age:
21 }); //
"<tr><td>Sam</td><td>21</td></tr>"
Templates are especially powerful in combination with Prototype's capability to insert content into the DOM. For example:
// <table id="people" border="1"></table>
var row = new
Template('<tr><td>#{name}</td><td>#{age}</td></tr>');
var people = [{name: 'Sam', age: 21}, {name: 'Marcel', age: 27}];
people.each(function(person){
new Insertion.Bottom('people',
row.evaluate(person));
});
Array and Enumerable Extensions
The String
prototype isn't the only language-native object that Prototype
enhances. It also extends JavaScript's Array
prototype with over a dozen
methods, including four in the latest release.
-
size()
returns the number of elements in the array. For example:[1,2,3].size(); // => 3
-
clone()
returns a clone of the array. For example:var a = [1, 2, 3];
var b = a;
b.reverse();
a; // => [3, 2, 1]
var a = [1, 2, 3];
var b = a.clone();
b.reverse();
a; // => [1, 2, 3] -
reduce()
returns the array untouched if it has more than one element. If it only has one element,reduce()
returns the element. For example:[1, 2].reduce(); // [1, 2]
[1].reduce(); // 1
[].reduce(); // undefined -
uniq()
returns a new array with duplicates removed. For example:[1, 3, 3].uniq(); // => [1, 3]
-
Although not technically an extension to the
Array
prototype, the new method$w(str)
creates arrays from strings, like Ruby's%w
method. For example:$w("foo bar baz"); // => ["foo","bar","baz"]
In addition to the extensions directly to Array
, Prototype also provides an
object called Enumerable
, inspired by the Ruby module of the same name. The
methods defined in Enumerable
are added to several type of collections,
including Array
, Hash
, and ObjectRange
. As with
Ruby's Enumerable
, it's possible to "mix-in" Prototype's
Enumerable
methods into your own custom classes as well. There are a handful
of new features added in the 1.5 release:
-
eachSlice(number[, iterator])
groups the members into arrays of size number (or less, if number does not divide the collection evenly.) If iterator is provided, it's called for each group, and the result is collected and returned.$R(1,6).eachSlice(3) // => [[1,2,3],[4,5,6]]
$R(1,6).eachSlice(4) // => [[1,2,3,4],[5,6]]
$R(1,6).eachSlice(3, function(g) { return g.reverse(); }) // => [[3,2,1],[6,5,4]] -
inGroupsOf(number[, fillWith])
groups the members into arrays of size number (padding any remainder slots with null or the string fillWith).[1,2,3,4,5,6].inGroupsOf(3); // => [[1,2,3],[4,5,6]]
$R(1,6).inGroupsOf(4); // => [[1,2,3,4],[5,6,null,null]]
$R(1,6).inGroupsOf(4, 'x') // => [[1,2,3,4],[5,6,"x","x"]] -
size()
returns the number of elements in the collection.$R(1,5).size(); // => 5
-
each()
now returns the collection to allow for method chaining.$R(1,3).each(alert).collect(function(n){ return n+1; }); // => [2,3,4]
DOM Access
The area that has gotten the most attention in the 1.5 release is Prototype's DOM access and manipulation methods.
First, a new Selector
class has been added for matching elements by CSS
selector tokens. The new $$()
function provides easy access to the feature,
returning DOM elements that match simple CSS selector strings. For example:
// Find all <img> elements inside <p> elements with class "summary", all
inside the <div> with id "page". Hide each matched <img> tag:
$$('div#page p.summary img').each(Element.hide)
// Supports attribute selectors:
$$('form#foo input[type=text]').each(function(input) {
input.setStyle({color: 'red'});
});
Support Insertion.Before
and Insertion.After
for <tr>
elements in IE.
Add Element.extend
, which mixes Element
methods into a single
HTML element. This means you can now write $('foo').show()
instead of
Element.show('foo')
. $()
, $$()
and
document.getElementsByClassName()
automatically call
Element.extend
on any returned elements. Plus, all destructive Element
methods (i.e., those methods that change the element rather than return some value)
now
return the element itself—meaning that Element methods can be chained together. For
example:
$("sidebar").addClassName("selected").show();
The 1.5 release brought a ton of new methods to Element.Methods
:
-
replace(html)
is a cross-browser implementation of theouterHTML
property; replaces the entire element (including its start and end tags) with html. For example:$('target').replace('<p>Hello</p>');
-
toggleClassName(className)
adds or removes the class className to/from the element. For example:$('target').toggleClassName('active');
-
getWidth()
returns the width of the element in pixels. For example:$('target').getWidth();
-
getElementsByClassName(className)
returns an array of all descendants of the element that have the class className. For example:$('target').getElementsByClassName('foo');
-
getElementsBySelector(expression1[, expression2 [...])
returns an array of all descendants of the element that match any of the given CSS selector expressions. For example:$('target').getElementsBySelector('.foo');
$('target').getElementsBySelector('li.foo', 'p.bar'); -
childOf(ancestor)
returns true when the element is a child of ancestor. For example:$('target').childOf($('bar')); // => false
-
inspect()
returns a string representation of the element useful for debugging, including its name, id, and classes. For example:$('target').inspect(); // => '<div id="target">'
-
ancestors()
,descendants()
,previousSiblings()
,nextSiblings()
, andsiblings()
return arrays of related elements. For example:$('target').ancestors();
$('target').descendants();
$('target').previousSiblings();
$('target').nextSiblings();
$('target').siblings(); -
immediateDescendants()
returns an array of the element's child nodes without text nodes. For example:$('target').immediateDescendants();
-
up([expression[, index]])
returns the first ancestor of the element that optionally matches the CSS selector expression. If index is given, it returns the nth matching element.$('target').up();
$('target').up(1);
$('target').up('li');
$('target').up('li', 1); -
down([expression[, index]])
returns the first child of the element that optionally matches the CSS selector expression. If index is given, it returns the nth matching element.$('target').down();
$('target').down(1);
$('target').down('li');
$('target').down('li', 1); -
previous([expression[, index]])
returns the previous sibling of the element that optionally matches the CSS selector expression. If index is given, it returns the nth matching element.$('target').previous();
$('target').previous(1);
$('target').previous('li');
$('target').previous('li', 1); -
next([expression[, index]])
returns the next sibling of the element that optionally matches the CSS selector expression. If index is given, it returns the nth matching element.$('target').next();
$('target').next(1);
$('target').next('li');
$('target').next('li', 1);
-
match(selector)
takes a single CSS selector expression (orSelector
instance) and returnstrue
if it matches the element. For example:$('target').match('div'); // => true
-
readAttribute(name)
returns the value of the element's attribute named name. Useful in conjunction withEnumerable.invoke
for extracting the values of a custom attribute from a collection of elements. For example:// <div id="widgets">
// <div class="widget" widget_id="7">...</div>
// <div class="widget" widget_id="8">...</div>
// <div class="widget" widget_id="9">...</div>
// </div>$$('div.widget').invoke('readAttribute', 'widget_id') // ["7", "8", "9"]
-
update(html)
replaces theinnerHTML
property of the element with html. If html contains<script>
blocks, they will not be included, but they will be evaluated. As of version 1.5,update()
works with table-related elements and can take a nonstring parameter, or none at all. For example:$('target').update('Hello');
$('target').update() // clears the element
$('target').update(123) // set element content to '123' -
hasAttribute(attribute) returns whether the element has an attribute named attribute. Despite being a standard DOM method, not all browsers implement this method, so Prototype spans the gap.
For example:$('target').hasAttribute('href');
-
While the members of
Element.Methods
are added to every element, form-related elements also get the methods defined inForm.Methods
andForm.Element.Methods
. Two new such methods were added toForm.Element.Methods
in Prototype 1.5:enable()
anddisable()
, which make the element editable or locked. For example:$('target').enable();
$('target').disable(); -
Backward compatibility change:
toggle()
,show()
, andhide()
, as well asField.clear()
andField.present()
, no longer take an arbitrary number of arguments. Before upgrading to Prototype 1.5, check your code for instances like this:Element.toggle('page', 'sidebar', 'content') // Old way; won't work in 1.5
['page', 'sidebar', 'content'].each(Element.toggle) // New way; 1.5-compatibleElement.show('page', 'sidebar', 'content') // Old way; won't work in 1.5
['page', 'sidebar', 'content'].each(Element.show) // New way; 1.5-compatibleElement.hide('page', 'sidebar', 'content') // Old way; won't work in 1.5
['page', 'sidebar', 'content'].each(Element.hide) // New way; 1.5-compatible
...And More!
-
Object.clone(object)
returns a shallow clone of object, such that the properties of object that are themselves objects are not cloned. For example:original = {name: "Sam", age: "21", car:{make: "Honda"}};
copy = Object.clone(original);
copy.name = "Marcel";
copy.car.make = "Toyota";
original.name; // "Sam"
original.car.make; // "Toyota" -
Object.keys(object)
returns an array of the names of object's properties and methods. For example:Object.keys({foo:'bar'}); // => ["foo"]
-
Object.values(object)
returns an array of the values of object's properties and methods. For example:Object.values({foo:'bar'}); // => ["bar"]
-
Instances of
PeriodicalExecutor
have a new method,stop()
, which will stop performing the periodical tasks. After stopping, the object will call the callback given in theonComplete
option, if any. -
Function.prototype.bindAsEventListener()
now takes an arbitrary number of arguments. Any additional arguments after the first (an object) will be passed through as arguments to the function. -
The Prototype developers maintain a suite of unit tests alongside the library itself, verifying that the code works—and keeps working—across a range of browsers. The test coverage in this release has skyrocketed, with an incredible 20-fold increase in the number of assertions. The testing infrastructure has matured remarkably as well: with one shell command (
rake test
, which requires Ruby and the Rake library), Prototype's tests are automatically run in every browser found on your system, and the results are displayed. You take advantage of the same infrastructure to test your own JavaScript, thanks to unittest.js, part of the script.aculo.us library. The status of the JavaScript test run can even be automatically integrated with the other unit tests in your system. If you are building a JavaScript-heavy web application, that safety net can be a life-saver (or more likely, a job-saver.) -
In addition to all of the new features described above, this release includes 68 bug fixes, cross-platform compatibility enhancements and performance improvements. And over 60 contributors were acknowledged—all of whom deserve credit for making this release such a remarkable one.
In this article, we've looked at all of the significant additions in Prototype 1.5. It's a wealth of new features, but it scarcely compares to the full breadth of Prototype's functionality. With just a few exceptions, all of the method descriptions and examples in this guide were drawn from my Prototype Quick Reference, which covers the entire library in just as much detail. The same content is available in print as part of my book, Ajax on Rails.