Developer Notes for prototype.js

This guide is based on the Developer Notes for prototype.js by Sergio Pereira.

What is that?

In case you haven't already used it, prototype.js is a JavaScript library written by Sam Stephenson. This amazingly well thought and well written piece of standards-compliant code takes a lot of the burden associated with creating rich, highly interactive web pages that characterize the Web 2.0 off your back.

If you tried to use this library recently, you probably noticed that documentation is not one of its strongest points. As many other developers before me, I got my head around prototype.js by reading the source code and experimenting with it. I thought it would be nice to take notes while I learned and share with everybody else.

As you read the examples and the reference, developers familiar with the Ruby programming language will notice an intentional similarity between Ruby's built-in classes and many of the extensions implemented by this library.

Using the $() function

The $() function is a handy shortcut to the all-too-frequent document.getElementById() function of the DOM. Like the DOM function, this one returns the element that has the id passed as an argument.

Unlike the DOM function, though, this one goes further. You can pass more than one id and $() will return an Array object with all the requested elements. The example below should illustrate this.

<com:TClientScript UsingClientScripts="prado" />

This is a paragraph

This is another paragraph

Another nice thing about this function is that you can pass either the id string or the element object itself, which makes this function very useful when creating other functions that can also take either form of argument.

Using the $F() function

The $F() function is a another welcome shortcut. It returns the value of any field input control, like text boxes or drop-down lists. The function can take as argument either the element id or the element object itself.

Using the $A() function

The $A() function converts the single argument it receives into an Array object.

This function, combined with the extensions for the Array class, makes it easier to convert or copy any enumerable list into an Array object. One suggested use is to convert DOM NodeLists into regular arrays, which can be traversed more efficiently. See example below.

Using the $H() function

The $H() function converts objects into enumerable Hash objects that resemble associative arrays.

function testHash() { //let's create the object var a = { first: 10, second: 20, third: 30 }; //now transform it into a hash var h = $H(a); alert(h.toQueryString()); //displays: first=10&second=20&third=30 }

Enumerating... Wow! Damn! Wahoo!

We are all familiar with for-loops. You know, create yourself an array, populate it with elements of the same kind, create a loop control structure (for, foreach, while, repeat, etc,) access each element sequentially, by its numeric index, and do something with the element.

When you come to think about it, almost every time you have an array in your code it means that you'll be using that array in a loop sooner or later. Wouldn't it be nice if the array objects had more functionality to deal with these iterations? Yes, it would, and many programming languages provide such functionality in their arrays or equivalent structures (like collections and lists.)

Well, it turns out that prototype.js gives us the Enumerable object, which implements a plethora of tricks for us to use when dealing with iterable data. The prototype.js library goes one step further and extends the Array class with all the methods of Enumerable.

Loops and iterator

In standard javascript, if you wanted to sequentially display the elements of an array, you could very well write something like this.

With our new best friend, prototype.js, we can rewrite this loop like this.

function showList() { var simpsons = ['Homer', 'Marge', 'Lisa', 'Bart', 'Meg']; simpsons.each( function(familyMember) { alert(familyMember); }); }

You are probably thinking "big freaking deal...just a weird syntax for the same old thing." Well, in the above example, yes, there's nothing too earth shattering going on. After all, there's not much to be changed in such a drop-dead-simple example. But keep reading, nonetheless.

Before we move on. Do you see this function that is being passed as an argument to the each method? Let's start referring to it as an iterator function.

Your arrays on steroids

Like we mentioned above, it's very common for all the elements in your array to be of the same kind, with the same properties and methods. Let's see how we can take advantage of iterator functions with our new souped-up arrays.

Finding an element according to a criteria.

Now let's kick it up another notch. See how we can filter out items in arrays, then retrieve just a desired member from each element.

This <a href="http://othersite.com/page.html">text</a> has a <a href="#localAnchor">lot</a> of <a href="#otherAnchor">links</a>. Some are <a href="http://wherever.com/page.html">external</a> and some are <a href="#someAnchor">local</a>

It takes just a little bit of practice to get completely addicted to this syntax. Next we will go through the available functions with the following example.

Enumerable Functions

The sample data for the following examples. var Fixtures = { Products: [ {name: 'Basecamp', company: '37signals', type: 'Project Management'}, {name: 'Shopify', company: 'JadedPixel', type: 'E-Commerce'}, {name: 'Mint', company: 'Shaun Inman',type: 'Statistics'} ], Artist: [ 'As I Lay Dying', '36 Crazyfist', 'Shadows Fall', 'Trivium', 'In Flames' ], Numbers: [0, 1, 4, 5, 98, 32, 12, 9] }; var F = Fixtures;

Enumerable.each function

I used to find myself writing a lot of for loops. Although, Prototype does not by any means eliminate the need to do for-loops, it does give you access to what I consider to be a cleaner, easier to read method in each. for(var i = 0; i < F.Numbers.length; i++) { Logger.info(F.Numbers[i]); }

The each function allows us to iterate over these objects Ruby style.

F.Numbers.each(function(num) { Logger.info(num); }); //Output 0 1 4 5 98 32 12 9

The each function takes one argument, an iterator function. This iterator is invoked once for every item in the array, and that item along with the optional index is passed to the iterator. So if we also needed the index we could do something like the code below.

F.Numbers.each(function(num, index) { Logger.info(index + ": " + num); }); //Output 0: 0 1: 1 2: 4 3: 5 4: 98 5: 32 6: 12 7: 9

Hash key/value pairs

Hashes can be created by wrapping an Object (associative array) in $H() and can have their key/value pairs exposed.

$H(F.Products[0]).each(function(product) { Logger.info(product.key + ": " + product.value); }); //Outputs name: Basecamp company: 37signals type: Project Management

We can also directly access the keys and values of a Hash without iterating over it.

$H(F.Products[1]).keys(); //Outputs name,company,type $H(F.Products[1]).values(); //Outputs Shopify,JadedPixel,E-Commerce

Enumerable.collect function

The collect function allows you to iterate over an Array and return the results as a new array. Each item returned as a result of the iteration will be pushed onto the end of the new array.

var companies = F.Products.collect(function(product) { return product.company; }); Logger.info(companies.join(', ')); // Outputs // 37signals, JadedPixel, Shaun Inman

You can even join on the end of the block.

return F.Products.collect(function(product) { return product.company; }).join(', ');

Enumerable.include function

The include function allows you to check if a value is included in an array and returns true or false depending on if a match was made. Assuming I put up a form asking the user to name some artist in my iTunes playlist, we could do something like the code below. Prime candidate for some conditional madness.

return F.Artists.include('Britney Spears'); // returns false

Enumerable.inject function

The inject function is good for getting a collective sum from an array of values. For instance, to add up all the numbers.

var score = F.Numbers.inject(0, function(sum, value) { return sum + value; }); Logger.info(score); //Output 161

The first argument to inject is just an initial value that would be added to the sum, so if we added 1 instead of 0, the output would be 162.

Enumerable.findAll function

When given an Array, the findAll function will return an array of items for which the iterator evaluated to true. Basically, it allows you to build a new array of values based on some search criteria. If we wanted to find all products whose type was “E-Commerce” we could do something like the code below.

var ecom = F.Products.findAll(function(product) { return product.type == 'E-Commerce'; }); Logger.info(ecom[0].company + " produces " + ecom[0].name); //Outputs JadedPixel produces Shopify

Note that even if only one match is made, just as in this case, the result is still returned as an array. In that case, ecom.company would return undefined.

Enumerable.detect function

Unlike the findAll function, the detect function will only return the first item for which the expression inside the iterator is true. So, if we wanted to find the first number that was greater than 5 we’d do something like the code below.

var low = F.Numbers.detect(function(num) { return num > 5 }); Logger.info(low); //Outputs 98

Even though, there are other numbers above 5 in our array, detect only gives us the first match back.

Enumerable.invoke function

The invoke function allows us to pass a method as a string and have that method invoked. For instance, if we wanted to sort our array of artists we’d do something like this:

[F.Artists].invoke('sort') //Outputs 36 Crazyfist,As I Lay Dying,In Flames,Shadows Fall,Trivium

Why not just use F.Artists.sort? Well, for the example above we could do just that, but here is where invoke shines.

[F.Artists, F.Letters].invoke('sort'); //Outputs 36 Crazyfist,As I Lay Dying,In Flames,...

So we invoked sort for each sub-array. Note that the code below will not work.

F.Artists.invoke('sort');

The reason this will not work is because it is taking each item in that array and trying to apply sort to it, thus if we wrote it outright, it would look something like this:

"36 Crazy Fists".sort();

We could however do something like this:

F.Artists.invoke('toLowerCase'); //Outputs 36 crazyfist,as i lay dying,in flames,shadows ...

Now, what about passing arguments to the invoke function? The first argument passed to invoke is the method to be invoked, and any other arguments beyond that will be passed as arguments to the invoked method.

F.Artists.invoke('concat', " is awesome ") //Outputs 36 Crazyfist is awesome ,As I Lay Dying is awesome ,...