Menu

What's New in Prototype 1.5?

January 24, 2007

Scott Raymond

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 the requestHeaders option. For example:

    new Ajax.Request('/data', { requestHeaders:{ Accept:'text/plain' } });

  • The contentType option sets the Content-Type request header, which defaults to application/x-www-form-urlencoded. An encoding option can also be specified, which defaults to UTF-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 HTTP GET and POST methods, but RESTfully-designed web applications often call for the lesser-used methods, like PUT and DELETE. Until browsers support the full range of HTTP methods, Prototype offers a compromise: "tunneling" those methods over POST, by including a _method query parameter with the request. You can now specify the intended HTTP method with the method option on all Ajax functions (the default is POST). Methods other than GET or POST will actually be requested with POST, 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 a Template string (see the Template 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 to gsub(), 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, converts CamelCase to camel_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 for String 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 the outerHTML 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(), and siblings() 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 (or Selector instance) and returns true 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 with Enumerable.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 the innerHTML 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 in Form.Methods and Form.Element.Methods. Two new such methods were added to Form.Element.Methods in Prototype 1.5: enable() and disable(), which make the element editable or locked. For example:

    $('target').enable();
    $('target').disable();

  • Backward compatibility change: toggle(), show(), and hide(), as well as Field.clear() and Field.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-compatible

    Element.show('page', 'sidebar', 'content') // Old way; won't work in 1.5
    ['page', 'sidebar', 'content'].each(Element.show) // New way; 1.5-compatible

    Element.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 the onComplete 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.