JavaScript Folder Collapser for Dynamic Content (Tutorial)

In this tutorial, I’ll show you how to create a group of collapsible folders using pure JavaScript and some jQuery added to the final product to give it a little bit of flare. The code will be structured in such a way that allows for implementation into a dynamic page — wherein the number of folders may be unknown.

First, it’s good to see exactly what we’ll be accomplishing, in case some of you are wondering what I mean when I say “collapsible folders”. Here is the demo page:

JavaScript Folder Collapser Demo (opens in a new window)

While I’m using the example of graphical folders, the same technique could be applied to navigation bars, promotional content areas, collapsible boxes, and more.

With the completed code we want to accomplish the following:

  • Keep the JavaScript code unobtrusive
  • Ensure that all folder contents are visible when JavaScript is turned off
  • Abstract the code into a reusable function so that it can be implemented easily in future projects

Step 1

First, let’s take a look at our clean, semantically correct XHTML, so we can assess what exactly we’ll be dealing with:

<ul>
	<li><a href="#" class="folder_link" id="folder_link_1">Folder 1</a>
		<div class="folder_content" id="folder_content_1">
		<p>This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here.</p>
		</div>
	</li>
	<li><a href="#" class="folder_link" id="folder_link_2">Folder 2</a>
		<div class="folder_content" id="folder_content_2">
		<p>This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here.</p>
		</div>
	</li>
	<li><a href="#" class="folder_link" id="folder_link_3">Folder 3</a>

		<div class="folder_content" id="folder_content_3">
		<p>This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here.</p>
		</div>

	</li>
	<li><a href="#" class="folder_link" id="folder_link_4">Folder 4</a>
		<div class="folder_content" id="folder_content_4">
		<p>This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here.</p>
		</div>
	</li>	
	<li><a href="#" class="folder_link" id="folder_link_5">Folder 5</a>
		<div class="folder_content" id="folder_content_5">
		<p>This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here. This is the content that goes in the folder. Text is added for the purpose of taking up space. Nothing special here.</p>
		</div>
	</li>
</ul>

And the CSS we’re concerned with looks like this:

ul {
	margin: 0 auto 0 auto;
	width: 330px;
}

 	ul li {
		padding: 20px;	
	}
	
	ul li a, ul li a:link, ul li a:visited {
		outline: none;
		text-align: rIght;
	}

a.folder_link {
	display: block;
	width: 270px;
	height: 87px;
	background: url(images/expanded.jpg) no-repeat 0 -87px;
	color: #838383;
	font-size: 24px;
	text-decoration: none;
	line-height: 87px;
}

	a.folder_link:hover {
		background-position: 0 0;
		text-decoration: underline;
	}

a.folder_link_collapsed {
	background: url(images/collapsed.jpg) no-repeat 0 -87px;
}

a.folder_link_expanded {
	background: url(images/expanded.jpg) no-repeat 0 -87px;
}
	
div.folder_content {
	padding: 0 0 0 25px;
}

A few things to note about the XHTML and CSS above:

  • The folders are in an unordered list
  • All content is visible without any JavaScript applied, to ensure the page degrades gracefully
  • The links are using CSS sprites, with two possible classes: folder_link_expanded and folder_link_collapsed
  • The links and content area have shared class names, so we can affect all the folders at the same time
  • The links and content have unique IDs, which can be produced dynamically via a back-end language like PHP

Everything else in the XHTML and CSS is just for presentational purposes and will not be affected by our JavaScript.

So let’s begin coding our collapser!

Step 2

The behaviour that we want to respond to, of course, is the clicking of any folder. Instead of identifying each of the links via their ID, which is feasible, we’ll do it by collecting all the links via their shared class name.

Using getElementsByTagName, we’ll collect in a variable all the links on the page, then we’ll loop through them:

myLinkCollection = document.getElementsByTagName("a");

for (var i = 0; i < myLinkCollection.length; i++)
	{

	}

With the code above, every link on the page -- not just the folder links -- will be accessible via our loop. So if we were to alert the words "hello world" inside of that loop, we would see five alert messages appear one after the other, equalling one message for every link on the page -- which in this case is five.

Step 3

But we don't want to manipulate all the links on the page; we want to manipulate each link that has the class "folder_link" applied to it. So, for each link in our loop, we'll check to see if the className property of that link has a value of "folder_link":

myLinkCollection = document.getElementsByTagName("a");

for (var i = 0; i < myLinkCollection.length; i++)
	{
		if (myLinkCollection[i].className == "folder_link")
			{

			}

	}

Thus, if we alert a message inside of the new if statement, the message will appear five times, since all five links on the page have the same class name that we're looking for. If we were to add more links elsewhere on the page, and those links did not have the same class name, then those new links would not qualify inside of our loop -- the number would still remain five.

Step 4

Since our loop has now identified the correct group of links, let's add an onclick event to each link that has a class of "folder_link":

myLinkCollection = document.getElementsByTagName("a");

for (var i = 0; i < myLinkCollection.length; i++)
	{
		if (myLinkCollection[i].className == "folder_link")
			{

				myLinkCollection[i].onclick = function()
					{
					// do something
					}
			}

	}

To summarize what we've done so far, here's the "English" translation of this code:

Loop through every anchor tag on the page; if someone clicks a link that has a class name equal to "folder_link", then "do something".

Step 5

But before we act on that click event, we want to ensure that all of our folders begin in the "collapsed", or closed mode. Remember that our CSS causes all the folders to be fully visible, or "expanded", by default. So, before our click event, we place the following line of code:

myLinkCollection[i].className = "folder_link folder_link_collapsed";

This ensures that all links on the page whose class is "folder_link" will have the additional class of "folder_link_collapsed" added, to "collapse" them by default for users with JavaScript enabled.

Step 6

Next, we'll "do something" to the links that qualify inside of our loop. The CSS is what controls whether or not our folder is in its "collapsed" state or in its "expanded" state, so we need to check the class name, and deal with it according to its value. In this case, we already know what the class name is, because we set it. But that class name will change with each click, so technically we don't really know what the class name is, we only know what it is before anything is ever clicked.

Inside of our onclick function we check the following:

if (this.className == "folder_link folder_link_collapsed")
	{
	//do something
	}
	else
	{
	//do something else
	}

With the above code added inside our loop, we now have two behaviours: What to do when the folder is "collapsed"; and what to do when the folder is not "collapsed", i.e. it's "expanded". There's just one problem: We don't know which link has been clicked, because they share common class names. This is where we implement the IDs, which, as we mentioned, could have been built dynamically by the back-end. So theoretically, we might not know how many folders are on the page.

Step 7

Before we check the class name of the current link, we want to find the ID of the link that was clicked, then strip the number from that ID so we can access the corresponding folder content. Here's how:

var myCurrentId = this.id;
var myCurrentNumber = myCurrentId.replace("folder_link_", "");

With the above code, which is placed right before our previous code block, we are storing the ID of the current anchor tag (using the this keyword) in a variable called myCurrentId. Then, using the JavaScript replace method, we strip "folder_link_" from the ID, leaving just a number, which we store in a variable called myCurrentNumber. So, for example, if the user clicks the third folder link, this variable would be equal to the string "3". We'll use this value to affect the appropriate folder content in our if / else statement:

if (this.className == "folder_link folder_link_collapsed")
	{
	document.getElementById("folder_content_"+myCurrentNumber)).style.display = "block";
	this.className = "folder_link folder_link_expanded";
	}
	else
	{
	document.getElementById("folder_content_"+myCurrentNumber)).style.display = "none";
	this.className = "folder_link folder_link_collapsed";
	}

So, if the user clicks a link that has a class name of "folder_link_collapsed", the content of that folder will be made visible by setting the display of the folder content to "block". And since the only other option is that the class name would be "folder_link_expanded", then we can safely apply a display of "none" in our else branch.

Step 8

Our code is virtually done, but we need to add a few enhancements to ensure it works in the best way possible. First of all, as we said earlier, when JavaScript is not available, we are displaying all the content by default. So before we even collect our anchor tags, we want to hide all of the folder contents.

We can do this by collecting all the <div> tags on the page, looping through them and hiding all those that have a class of "folder_content":

	var myContentCollection = document.getElementsByTagName("div");
	
	for (var i = 0; i < myContentCollection.length; i++) {
		if (myContentCollection[i].className == "folder_content") {
			myContentCollection[i].style.display = "none";
		}
	}

We also want to ensure that all of our folder links "return false", so the href value of our links is not actually visited. This will prevent the page from "jumping" due to the "#" in the href. We do this by simply adding return false at the end of the anonymous onclick function.

Step 9

Now, for aesthetic purposes, we'll use jQuery to hide and show the folders, to make the collapse and expand events a little more interesting. Here's our if / else branch again with jQuery added:

if (this.className == "folder_link folder_link_collapsed")
	{
	$("#folder_content_"+myCurrentNumber).slideDown("slow");
	this.className = "folder_link folder_link_expanded";
	}
	else
	{
	$("#folder_content_"+myCurrentNumber).slideUp("slow");
	this.className = "folder_link folder_link_collapsed";
	}

Since we're including the jQuery library file in this page, we can place all of our code in a function called "folderCollapser" and use jQuery's "document ready" function to call the "folderCollapser" function. Here is our near-completed code:

$(document).ready(function() {
	folderCollapser();
});

function folderCollapser() {
	
	var myContentCollection = document.getElementsByTagName("div");
	
	for (var i = 0; i < myContentCollection.length; i++) {
		if (myContentCollection[i].className == "folder_content") {
			myContentCollection[i].style.display = "none";
		}
	}
	
	myLinkCollection = document.getElementsByTagName("a");
	
	for (var i = 0; i < myLinkCollection.length; i++)
	{
		if (myLinkCollection[i].className == "folder_link")
			{
				myLinkCollection[i].className = "folder_link folder_link_collapsed";
				myLinkCollection[i].onclick = function()
					{
					var myCurrentId = this.id;
					var myCurrentNumber = myCurrentId.replace("folder_link_", "");
					
					if (this.className == "folder_link folder_link_collapsed")
						{
						$("#folder_content_"+myCurrentNumber).slideDown("slow");
						this.className = "folder_link folder_link_expanded";
						}
						else
						{
						$("#folder_content_"+myCurrentNumber).slideUp("slow");
						this.className = "folder_link folder_link_collapsed";
						}
					return false;

					}

			}

	}
}

Step 10

We could leave the code this way, and it would work fine, but it wouldn't be very easy to reuse. We would have to manually go in and change any class names and IDs to correspond to a newly created page. So, our final step is to completely abstract our function and place all of our class and ID values in variables at the top of our function. Here is the fully-functional, reusable code:


$(document).ready(function() {
	folderCollapser();
});

function folderCollapser() {
	
	var myContentCollection = document.getElementsByTagName("div");
	
	for (var i = 0; i < myContentCollection.length; i++) {
		if (myContentCollection[i].className == "folder_content") {
			myContentCollection[i].style.display = "none";
		}
	}
	
	var myUnitClass = "folder_link";
	var myUnitLinkCollapsed = "folder_link_collapsed";
	var myUnitLinkExpanded = "folder_link_expanded";
	var myUnitContent = "folder_content";
	var myAnimationSpeed = "slow";

	myLinkCollection = document.getElementsByTagName("a");
	
	for (var i = 0; i < myLinkCollection.length; i++)
	{
		if (myLinkCollection[i].className == myUnitClass)
			{
				myLinkCollection[i].className = myUnitClass + " " + myUnitLinkCollapsed;
				myLinkCollection[i].onclick = function()
					{
					var myCurrentId = this.id;
					var myCurrentNumber = myCurrentId.replace(myUnitClass + "_", "");
					
					if (this.className == myUnitClass + " " + myUnitLinkCollapsed)
						{
						$("#" + myUnitContent + "_" + myCurrentNumber).slideDown(myAnimationSpeed);
						this.className = myUnitClass + " " + myUnitLinkExpanded;
						}
						else
						{
						$("#" + myUnitContent + "_" + myCurrentNumber).slideUp(myAnimationSpeed);
						this.className = myUnitClass + " " + myUnitLinkCollapsed;
						}
					return false;

					}

			}

	}
}

Our function is now completely abstract, allowing all the class names and IDs to be set in the variables at the top, ready to be reused.

Hope you enjoyed this tutorial. Maybe another time, I'll try writing the whole thing in jQuery, to see how many lines of code I can save!

Advertise Here

4 Responses

  1. terrific script, used it for my personal files page for university. Although I have a hard time following the last few steps, its pretty portable so i guess i dont have to understand it if it works! anyway… thanks so much!

  2. Nickolay:

    Very nice tutorial!

    I like how lightly you take the reader into the depths of things and how well you reason every code transformation.
    Thank you for that.

    You’ve especially helped me in building an accessible page, visible to no-javascript browsers and in using of JQuery which adds that “special” ability I was looking for.

    Wish I could search your advise on other similar topics…

  3. Very nice tutorial!

    terrific script, used it for my personal files page for university. Although I have a hard time following the last few steps, its pretty portable so i guess i dont have to understand it if it works! anyway — thanks so much!

  4. Great, i really like the folders, and their effect
    i just added a link to your scrip from my web site
    http://www.ajaxshake.com
    Thanks

Leave a Reply

Comment Rules: Please use a real name or alias. Keywords are not allowed in the "name" field. If you use keywords, your comment will be deleted, or your name will be replaced with the alias from your email address. No foul language, please. Thank you for cooperating.

Instructions for code snippets: Wrap inline code in <code> tags; wrap blocks of code in <pre> and <code> tags. When you want your HTML to display on the page in a code snippet inside of <code> tags, make sure you use &lt; and &gt; instead of < and >, otherwise your code will be eaten by pink unicorns.