X Tutorial: Collapsible Sections

Let's develop an unobtrusive page-enhancement using the X Library.

Enhance What?

First we need a web page to work with. You can't develop a page-enhancement without a page, LOL! When I am starting a new demo I usually start with a copy of one of these templates: html4_strict_template.html or html5_template.html. There are other template files in that same directory. We will start this project with the following HTML file. Its name will be "demo1.html". It has a very simple, CSS-controlled layout.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>X Quick-Start Demo</title>
<meta name='author' content='Mike Foster (Cross-Browser.com)'>
<meta name='description' content='This is the HTML for the tutorial.'>
<link rel='stylesheet' type='text/css' href='demo1.css'>
</head>
<body>
<div id="layout-container">
  <div id="layout-header">
    <h1>Cross-Browser.com</h1>
    <p>X Quick-Start Demo</p>
  </div>
  <div id="layout-hmenu">
    <ul>
      <li><a href="http://cross-browser.com/">Home</a></li>
      <li><a href="http://cross-browser.com/x/docs/x_quickstart.php">X Quick-Start</a></li>
      <li><a href="http://cross-browser.com/x/lib/">X Viewer</a></li>
    </ul>
  </div>
  <div id="layout-main-col">
    <h2 class='collapsible'>H2 Heading</h2>
    <div>
      <h3 class='collapsible'>H3 Heading</h3>
      <div>
        <p>Lorem ipsum dolor sit amet...</p>
        <p>Nam ornare, felis non fauc...</p>
      </div>
      <h3 class='collapsible'>H3 Heading</h3>
      <div>
        <h4 class='collapsible'>H4 Heading</h4>
        <div>
          <p>Aenean tempor. Mauris to...</p>
          <p>Nulla feugiat hendrerit ...</p>
        </div>
        <h4 class='collapsible'>H4 Heading</h4>
        <div>
          <p>Nulla a lacus. Nulla fac...</p>
          <p>Suspendisse dapibus, mag...</p>
        </div>
      </div>
    </div>
  </div>
  <div id="layout-footer">
    <p>Footer</p>
  </div>
</div>
</body>
</html>
  

Design

Let's come up with a brief description of what we want our enhancement to do.

  • We want the script to place an icon to the left of heading elements. When the icon is clicked the section under the heading should collapse. When the icon is clicked again the section should expand.
  • We don't want this enhancement on all headings, just the ones we specify - and we will specify them by adding 'collapsible' to their class attribute.
  • The enhancement should be very easy to add to a web page. We don't want to have to modify Javascript every time we use this on a new page.

Implementation

After a browser completely loads a web page, as well as all CSS, Javascript and image files included by the page, the load event will then occur. This is when we will initialize our enhancement. We will use xAddEventListener to register a function which will be called when the load event occurs. Like all X functions, xAddEventListener is in the directory x/lib and we must now add a SCRIPT element to the HTML file which will cause that function's code to be loaded when the HTML file loads. However, in the x directory there is a prebuilt library file, "x.js". This file does not contain the entire X Library but it contains some of the most used functions. So we will add a SCRIPT element to cause the "x.js" file to be loaded when the HTML file loads.

Now we are ready to start writing the code for our enhancement - but where will we put this code? Let's put it in its own file and name it "demo1.js". So we need to add one more SCRIPT element to our HTML file to cause "demo1.js" to be loaded when the HTML file loads.

The following is the HEAD element from the "demo1.html" file, showing the SCRIPT elements that have been added. These files reside in the x/examples directory.

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>X Quick-Start Demo</title>
<meta name='author' content='Mike Foster (Cross-Browser.com)'>
<meta name='description' content='This is the HTML for the tutorial.'>
<link rel='stylesheet' type='text/css' href='demo1.css'>
<script type='text/javascript' src='../x.js'></script>
<script type='text/javascript' src='demo1.js'></script>
</head>
  

In the file "demo1.js" let's register a function to be called when the load event occurs. Use the X Viewer to see descriptions of the arguments we must provide to xAddEventListener.

xAddEventListener(window, 'load', initializeCollapsible, false);

function initializeCollapsible()
{
  //
}
  

Looking back at our design requirements we see that we need to get a list of all elements that have "collapsible" in their className property. Looking in the DOM section of the X Viewer Index we find xGetElementsByClassName, and that's just what we need.

So, using xGetElementsByClassName, we will get an array of elements and then loop through the array, as shown in the following.

xAddEventListener(window, 'load', initializeCollapsible, false);

function initializeCollapsible()
{
  var i, headings = xGetElementsByClassName('collapsible');
  for (i = 0; i < headings.length; i++) {
    //
  }
}
  

Looking back at our design requirements we see that we need to create an "icon" element for each element in the array. So let's create a DIV for the icon element and define two CSS classes for it, one for the collapse icon and one for the expand icon. These CSS classes can set a background image and also set the icon element's size to be the same as the image.

But how are we going to position the icon to the left of the heading itself? We could do it the hard way or the easy way. I like easy!

Let's back up for a minute and talk about the CSS position property. The default value for this property is "static" and it means the element will be positioned according to the flow of HTML. This is how all HTML elements are normally rendered, but if the position property has any other value then the element is considered to be positioned. If the element's position property has the value "relative" then the element will be rendered exactly as if its position property had been "static" but now the left and top properties will offset the element from its default (static) position. If the element's position property has the value "absolute" then the element will be positioned with respect to its nearest positioned ancestor. It is no longer in the flow.

We will use this to our advantage. We will insert the icon element into the heading element. We will give the heading element position:relative and give the icon element position:absolute. Now the browser will position the heading in the flow and position the icon with respect to the heading and all we have to do is move it to the left (a negative value) by a few pixels, and we can easily do that in the CSS. So let's look at the CSS for this project in the following.

.collapsible {
  position:relative;
}
.CollapseIcon, .ExpandIcon {
  position:absolute;
  left:-16px;
  top:5px;
  width:9px;
  height:9px;
  cursor:pointer;
}
.CollapseIcon {
  background-image:url("../../images/minus9x9.gif");
}
.ExpandIcon {
  background-image:url("../../images/plus9x9.gif");
}
  

Now we have a firm grasp on how this thing is going to work and the remaining Javascript should just fall right into place. In the following we create the icon element and insert it into the heading element.

xAddEventListener(window, 'load', initializeCollapsible, false);

function initializeCollapsible()
{
  var i, icon, headings = xGetElementsByClassName('collapsible');
  for (i = 0; i < headings.length; i++) {
    icon = document.createElement('div');
    headings[i].appendChild(icon);
  }
}
  

We've completed most of our enhancement's initialization. Now from the design requirements we see that a click on the icon must collapse or expand the icon's corresponding section. We will define a "section" to be the first element following the heading. Looking at the HTML we see that a section is the next sibling of its heading. Go to the X Viewer and look in the DOM section and you will find a function named xNextSib. This is what we need to find a section element which follows a heading element. This function is not in the "x.js" file so we must add another SCRIPT element to the HTML file which causes the xNextSib code to get loaded. The following is the HEAD element from the "demo1.html" file, showing the SCRIPT elements that have been added.

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>X Quick-Start Demo</title>
<meta name='author' content='Mike Foster (Cross-Browser.com)'>
<meta name='description' content='This is the HTML for the tutorial.'>
<link rel='stylesheet' type='text/css' href='demo1.css'>
<script type='text/javascript' src='../x.js'></script>
<script type='text/javascript' src='../lib/xnextsib.js'></script>
<script type='text/javascript' src='demo1.js'></script>
</head>
  

An icon element must remember which section element it is associated with so that the section can be collapsed or expanded when the icon is clicked. We find the section element with xNextSib and then we assign it to a custom property on the icon element. We will name the custom property collapsibleSection, as follows.

xAddEventListener(window, 'load', initializeCollapsible, false);

function initializeCollapsible()
{
  var i, icon, headings = xGetElementsByClassName('collapsible');
  for (i = 0; i < headings.length; i++) {
    icon = document.createElement('div');
    icon.collapsibleSection = xNextSib(headings[i]);
    headings[i].appendChild(icon);
  }
}
  

Now we need to listen for a click event on an icon element. We will use the DOM0 instead of the DOM2 event interface. The DOM2 event interface (addEventListener) has the advantage of allowing more than one listener for the same event on the same element, but that is not an issue here because our code creates the icon elements. The DOM0 event interface has an advantage which we will see shortly.

xAddEventListener(window, 'load', initializeCollapsible, false);

function initializeCollapsible()
{
  var i, icon, headings = xGetElementsByClassName('collapsible');
  for (i = 0; i < headings.length; i++) {
    icon = document.createElement('div');
    icon.collapsibleSection = xNextSib(headings[i]);
    icon.onclick = iconOnClick;
    headings[i].appendChild(icon);
  }
}

function iconOnClick()
{
  //
}
  

So now the function iconOnClick will be called whenever an icon element is clicked. We will now see the advantage to using the DOM0 event interface here. In iconOnClick our code can refer to the keyword this which will reference the specific icon element that was clicked!

The listener's task is to either collapse or expand its associated section element. To accomplish collapsing and expanding we will simply set the section element's display property to "none" or "block". We will first look at the value of display to determine if we need to expand or collapse the section. We know which section to collapse or expand because, in the initialization loop, we saved a reference to it in the icon element's collapsibleSection property.

function iconOnClick()
{
  var section = this.collapsibleSection;
  if (section.style.display != 'block') {
    section.style.display = 'block';
  }
  else {
    section.style.display = 'none';
  }
}
  

If we just collapsed the section then we want to change the icon's class to "ExpandIcon". If we just expanded the section then we want to change the icon's class to "CollapseIcon". Also, it would be a nice touch to change the icon's title property to correspond to its class, so the user would get an appropriate tooltip when the mouse is over the icon.

function iconOnClick()
{
  var section = this.collapsibleSection;
  if (section.style.display != 'block') {
    section.style.display = 'block';
    this.className = 'CollapseIcon';
    this.title = 'Click to collapse';
  }
  else {
    section.style.display = 'none';
    this.className = 'ExpandIcon';
    this.title = 'Click to expand';
  }
}
  

It looks like we are finished - but we are not. Look closely at the initialization function as well as the CSS. Did I forget something?

What image will be initially displayed for the icons? What CSS class do the icons have initially? What is the initial value of an icon's title property? There is none! When the icons are first created (when the page first loads) they will have no background image and no value for the title property.

We could just add some code to the initialization loop that sets an icon's inital CSS class and title property... but notice that our code in the iconOnClick function does exactly that! So the initialization function can just call the icon's iconOnClick function to initialize its CSS class and title property.

function initializeCollapsible()
{
  var i, icon, headings = xGetElementsByClassName('collapsible');
  for (i = 0; i < headings.length; i++) {
    icon = document.createElement('div');
    icon.collapsibleSection = xNextSib(headings[i]);
    icon.onclick = iconOnClick;
    icon.onclick();
    headings[i].appendChild(icon);
  }
}
  

Look at the following, which is the second line in the iconOnClick function.

  if (section.style.display != 'block') {
  

Did I make a mistake here? What will be the value of a section element's initial display property? It will be an empty string! But obviously the section gets rendered, so its real display property must actually have the value "block" - and it does. However, the properties of element.style correspond to the HTML element's inline STYLE attribute - NOT to any styles applied by its id or class attributes. Therefore the value of a section element's initial display property will be an empty string. Now that if statement should make more sense.

This if statement is important although it appears to be trivial. If I were not also using iconOnClick to initialize the icon then I would have tested for != 'none' instead of testing for != 'block' and swapped the code blocks following the if statement.

We could stop here, but let's add one more feature. When the mouse is over an icon we can change the background of its section to give a visual indication of what will get collapsed if they click the icon.

The following is our entire "demo1.js" file.

xAddEventListener(window, 'load', initializeCollapsible, false);

function initializeCollapsible()
{
  var i, icon, headings = xGetElementsByClassName('collapsible');
  for (i = 0; i < headings.length; i++) {
    icon = document.createElement('div');
    icon.collapsibleSection = xNextSib(headings[i]);
    icon.onclick = iconOnClick;
    icon.onclick();
    icon.onmouseover = iconOnMouseover;
    icon.onmouseout = iconOnMouseout;
    headings[i].appendChild(icon);
  }
}

function iconOnClick()
{
  var section = this.collapsibleSection;
  if (section.style.display != 'block') {
    section.style.display = 'block';
    this.className = 'CollapseIcon';
    this.title = 'Click to collapse';
  }
  else {
    section.style.display = 'none';
    this.className = 'ExpandIcon';
    this.title = 'Click to expand';
  }
}

function iconOnMouseover()
{
  this.collapsibleSection.style.backgroundColor = '#cccccc';
}

function iconOnMouseout()
{
  this.collapsibleSection.style.backgroundColor = 'transparent';
}
  

The Demo

You can try out this enhancement and see all the source code here.

Conclusion

There is more we could do to this demo. You know that saying... software is never done ;-).

There is a more efficient way to utilize xGetElementsByClassName with its function call-back parameter.

We could put all the Javascript in one file instead of it being in separate files (XC automates this process).

This is a fun project and I'm sure you can think of more features to add.

Did you like this tutorial? Did you hate it? Did you add some more cool features to the demo? Come to the forums and tell us about it.

X Documentation

X Library Viewer - View documentation, source code, revision history and more for all X symbols.

X Quick-Start - Getting started with the X Library.

X Tutorial - Collapsible/expandable sections.

X Structure - Describes how an X symbol is defined by an xml and js file.

X Tools - Summary and revision history for the X build tool chain.

XAG Reference - X Library Aggregator.

XPP Reference - General Purpose Text Preprocessor.

About Cross-Browser.com

Cross-Browser.com is the home of X - a cross-browser Javascript library, and many demos, applications, articles and documentation.

Search

Cross-Browser.com

World Wide Web

License

By your use of X and/or CBE and/or any Javascript from this site you consent to the GNU LGPL - please read it. If you have any questions about the license, read the FAQ and/or come to the forums.

Tech Support

Forum support is available at the X Library Support Forums.

Browser Support

The X core is designed to work with all browsers, Object-detection instead of browser-detection is used exclusively. Currently, I'm testing with the following browsers. X has been tested by others on a wide variety of platforms.

Win7 (Home): IE 9.

WinXP (SP3): Chrome 3.0.195.38, Firefox 3.5.5, IE 7 & 8, Opera 10.60 and Safari 4.0.3.

Linux (Ubuntu 9.10): Chrome 4.0.249.43 and FireFox 3.5.7.