Overview
The following is an example of a simple reusable widget created using the Prototype JavaScript library. The widget is an 'accordian' - a widget composed of multiple sections, of which only one is open at a time. This type of widget is available in all sorts of applications, from Microsoft Outlook (circa Outlook 2000), to web applications, and more. In addition to showing off this simple widget, I'd like to walk through it to explain how it works and how to write something like this with Prototype. For all of Prototype's greatness, there is certainly a lack of documentation that makes it a little bit difficult to get into.
Example
Take a look at the example page I put together showing a few examples of accordians and their capabilities. This article will explain how to put together the class that does this.
Explaination: Creating Reusable Classes
I'll skip with going through the HTML - you can see that on the example page if you're interested - it would be helpful in understanding the javaScript here. In this example I've used version 1.5.0_rc0 of Prototype. The first line of the accordian.js file has the following:
-
Accordian = Class.create();
Prototype's Class.create() sets up Accordian as an object that can be instantiated. When the object is instantiated the 'initialize' method, its constructor, will be automatically called. You setup all of the methods within the object by defining its prototype - an object with properties and methods. This could be done in the following way:
-
Accordian.prototype = {
-
initialize: function(args) {
-
// do something
-
},
-
sectionClicked: function(args) {
-
-
},
-
openSection: function(args) {
-
-
},
-
closeExistingSection: function(args) {
-
-
}
-
}
This object could be instantiated in the following way:
-
var myAccordian = new Accordian(options);
Explanation: Writing the Constructor
The constructor needs to take two arguments - the ID of the element that contains the accordian and the type of entry that is used for the header of each section. The constructors main job is getting a list of all of the sections within the accordian and adds onClick event handlers to the headers so that clicking them will open the section. The constructor is shown below:
-
initialize: function(elem, clickableEntity) {
-
this.container = $(elem);
-
var headers = $$('#' + elem + ' .section ' + clickableEntity);
-
headers.each(function(header) {
-
Event.observe(header,'click',this.sectionClicked.bindAsEventListener(this));
-
}.bind(this));
-
}
Now to explain what's going on in the constructor:
- The first line defines the function that expects two variables - the ID of the element that holds the accordian and the type of element that will serve as the header for a section.
- The second line sets a class property, 'container', to the element passed in with variable 'elem'. It uses Prototype's $() function, which in this case is equivalent to document.getElementById()
- The third line uses the very powerful $$() function that returns an array of all elements that match a given css query. In this case, the query might look like '#myAccordian .section h3', which would return all h3 elements under an element with class 'section' under the element with ID 'myAccordian'.
- Lines 4-6 setup the event handlers that will be used when the user clicks the headers. This uses the each() function that Prototype adds to arrays in order to quickly loop through an array without using a for loop or creating a bunch of temporary variables. This function takes a function as an argument that will be applied as an iterator over the array. This use of this technique is similar to the foreach concept in PHP (foreach($headers as $header)) and other languages. Because functions are objects in JavaScript, we need to add a trick at the end of the each function defined within 'each' - we need to add 'bind(this)'. This means that within the new function, the 'this' keyword will be bound to the Accordian object, not to the function itself.
- Line 5 uses Prototype's Event class to setup the observer - in this case it will bind the classes sectionClicked method to any click on the header elements. Similar to the binding needed on the function, the reference to the sectionClicked function is follwed by 'bindAsEventListener(this)', which means that inside the event handler, 'this' will refer to the Accordian object, not to the clicked header element.
Explanation: Writing the Event Handler
The function referenced as the event handler in the constructor is shown below:
-
sectionClicked: function(event) {
-
this.openSection(Event.element(event).parentNode);
-
}
Functions called as an event always get passed an event object. This function takes this event and uses the Prototype 'Event' class to get a handle on the parent node of the element that was clicked, the section itself. It then passes this as an argument to another method in this class - openSection.
Explanation: Opening a Section
This function if fairly straightfoward - it checks to see if the section being opened is already open, and if not, calls a function to close the open section, and finally opens the new section. It is shown below:
-
openSection: function(section) {
-
var section = $(section);
-
if(section.id != this.currentSection) {
-
this.closeExistingSection();
-
this.currentSection = section.id;
-
var contents = document.getElementsByClassName('contents',section);
-
contents[0].show();
-
}
-
}
- The function takes one argument, the section that is to be opened
- The second line uses the $() function to get the DOM element. It is important to note that this function can either take the section ID as an argument or the actual section DOM node. The $() function adds this extra flexibility
- The third line checks to see whether the ID of the section is the same as the currently open section. If it is, no other logic is performed. If it isn't, the next few lines will open that section.
- After line 4 calls a function to close the existing section, line 5 sets a class property 'currentSection' to the new section
- Line 6 uses Prototype's getElementsByClassName, passing the class (contents) and the element under which it should look (the new section). This returns an array of elements
- Line 7 takes the first element with class contents (their should only be one), and uses Prototype's show() function to show the element
Explanation: Closing a Section
This last function is the simplest of all - it checks to see whether there is an open section, and if so, it hides it.
-
closeExistingSection: function() {
-
if(this.currentSection) {
-
var contents = document.getElementsByClassName('contents',this.currentSection);
-
contents[0].hide();
-
}
-
}
- The second line checks if the currentSection exists - it is only set after the first section has been opened
- The third line uses the same technique as in the previous function - it uses getElementsByClassName to get the elements with class 'section'
- Finally, the hide function will hide this element so that the section is collapsed
The Final Script
-
Accordian = Class.create();
-
Accordian.prototype = {
-
initialize: function(elem, clickableEntity) {
-
this.container = $(elem);
-
var headers = $$('#' + elem + ' .section ' + clickableEntity);
-
headers.each(function(header) {
-
Event.observe(header,'click',this.sectionClicked.bindAsEventListener(this));
-
}.bind(this));
-
},
-
sectionClicked: function(event) {
-
this.openSection(Event.element(event).parentNode);
-
},
-
openSection: function(section) {
-
var section = $(section);
-
if(section.id != this.currentSection) {
-
this.closeExistingSection();
-
this.currentSection = section.id;
-
var contents = document.getElementsByClassName('contents',section);
-
contents[0].show();
-
}
-
},
-
closeExistingSection: function() {
-
if(this.currentSection) {
-
var contents = document.getElementsByClassName('contents',this.currentSection);
-
contents[0].hide();
-
}
-
}
-
}
Conclusion
Though this is a fairly simple example, I hope it helped to explain some of the features of the Prototype library and can get people kickstarted to writing classes with JavaScript. Most of it is very simple...if only there were some real documentation. The best source right now is probably Developer notes for prototype.js, which covers features up to Prototype 1.4. Another site with a compilation of links is http://www.prototypedoc.com/.
Go forth and rid the world of nasty IE5 style, unreadable, browser-sniffing, hackish, global-scope JavaScript!
August 31st, 2006 at 10:35 am
Trackback did’t worked so here is my (german) Post about your great Tutorial:
http://blog.ginader.de/archives/2006/08/31/Akkordeon-Widged-mit-Prototype.php
September 6th, 2006 at 1:38 pm
Wow, nice work Greg. This information is bound to come in handy for my own projects.
September 13th, 2006 at 6:34 pm
It really looks simple and powerfull, I’m going to try it. Thank you for such wonder full article.
September 19th, 2006 at 3:02 pm
That’s some good technical writing, dude. Drop the prod mgr act and start up a company already. Or at least publish an online book, so i can ask for your digital autograph.
Ok, you can delete my comment now, so that I don’t get blasted by your serious readers… Good to know you’ve moved on and doing well.
October 6th, 2006 at 11:10 pm
This is such a well written article. Thanks. Am going to explore this now.
PS- Any chance you could get the scriptaculous cool sliding effects to make the windows slide open so that this is closer to the rico implementation?
October 21st, 2006 at 7:23 am
Hi Greg, it works perfect with Firefox on OSX but it doesn’t work with Safari.
Thanx for all the good articles.
October 21st, 2006 at 7:31 am
Sorry, it works on Safari. (when clicking NEXT TO (left or right) but not ON the text element)
October 21st, 2006 at 1:00 pm
@Hein - I think it is similar with internet explorer in that the header element click only seems to work when clicking the text part…even though it is a block level element. The simple workaround is to use a div instead….
October 24th, 2006 at 10:42 am
Draggables in accordeon need workaround
Nice example of using the prototype. I just recently started playing with draggables etc. from Scriptaculous (http://wiki.script.aculo.us/scriptaculous/) also based on the same prototype.js and was looking for a way to put all those draggables in an accordeon.
I used your accordeon with great success. I had to alter it because in Firefox once I opened a section it remained open, although I already opened another session. The reason for this is that the heigth of the div that is defined inside the sections always ‘reserves’ that height even if it not visible anymore (upon closure).
In your example that problem is not shown because you use overflow auto.
I can not use it because then my draggables can not leave the accordeon, so I did a little workaround,
namely set the height to 1 px upon closeure and set it to auto upon opening as in the coder here.
openSection: function(section) {
var section = $(section);
if(section.id != this.currentSection) {
this.closeExistingSection();
this.currentSection = section.id;
var contents = document.getElementsByClassName(’accord_contents’,section);
contents[0].show();
contents[0].style.height=”auto”;
contents[0].style.overflow = “visible”;
}
},
closeExistingSection: function() {
if(this.currentSection) {
var contents = document.getElementsByClassName(’accord_contents’,this.currentSection);
contents[0].style.height=”1px”;
contents[0].style.overflow = “auto”;
// contents[0].hide(); // no need to hide anymore
}
}
December 13th, 2006 at 2:01 pm
Cool function, I’ve seen this done in a very much similar way: http://moofx.mad4milk.net/
P.S. Don’t know if you intended it that way, but Accordion is spelled with an O. http://en.wikipedia.org/wiki/Accordion
July 19th, 2007 at 9:57 am
[…] found this little gem from Greg over at Graphics by Greg the other […]
August 8th, 2007 at 8:41 am
Does not work:
Parse error: syntax error, unexpected T_STRING, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or ‘}’ in /home/bookm6/public_html/tree3/db.php on line 5
November 28th, 2007 at 9:59 am
Thanks for the great tutorial. With this information I was able to write my own definition list based accordion control (only 2.1kb).
Keep up the good work, thanks again.
February 22nd, 2008 at 2:00 pm
Awesome script, thank you. Is there any way to have an item open by default? I’ve been playing and have this:
Event.observe(window,’load’,init,false);
function init() {
accordian = new Accordian(’sectionContainer’,'h3′);
var section = $(sTickets);
this.currentSection = section.id;
var contents = document.getElementsByClassName(’contents’,section);
contents[0].show();
}
But it requires me to click the open section before clicking another…