Tuesday, April 15, 2014

Tutorial: Build Multiple Collapsable Containers

Introduction
In this tutorial, I'm going to go over a basic system for building a multiple collapsible container system. You should be familiar with HTML, CSS, CSS selection techniques, and have a good understanding of JavaScript and jQuery.

Header Stuff

1. Let's get started with the CSS section of our document. We want our users to be able to hover over a section and realize that it's a clickable region.

h2 {
     color:#00C;
     cursor: pointer;
}
h2:hover {
     color:#00F;
     cursor: pointer;
}


2. Next, we'll set up the display behavior of the main container elements. We want these div elements to display on top of each other.

div.expandContainer {
    display: table-row;
    overflow: hidden;
}

The overflow property is there to prevent our animated features of the collapsible container from jumping. I was quite plagued by the container "popping" when it is expanded and collapsed.

3. For the final bit of CSS, we're going to set up the default display behavior of the content sections of our containers. For this tutorial, we'll set the height and opacity to 0 and animate them over a half second to become visible again.

div.expandContent {
    height: 0px;
    opacity: 0;
    padding: 0 5px;
}
div.container {padding: 5px;}

The last line that handles the class called container is just there to keep things pushed away from the edges of the document.

4. Before we leave the header section of our document, let's call jquery 1.10:

<script src="jquery-1.11.0.min.js"></script>

HTML
Alrighty then. Let's get the HTML going.

<div class="container">
    <div id="title-first" class="expandContainer">
        <h2>First Title</h2>
        <div id="content1" class="expandContent">
            <p><a href="#">Link to content 1</a> is all about stuff inside the first container</p>
        </div>
    </div>
    <div id="title-second" class="expandContainer">
        <h2>Second Title</h2>
        <div id="content2" class="expandContent">
            <p><a href="#">Link to content 2</a> is all about stuff inside the second container</p>
        </div>
    </div>
    <div id="title-third" class="expandContainer">
        <h2>Third Title</h2>
        <div id="content3" class="expandContent">
            <p><a href="#">Link to content 3</a> is all about stuff inside the third container</p>
        </div>
    </div>
</div>

1. First off, I put everything in a master container div element with a class called container. It's a good idea to "contain" structures like this for a multitude of reasons including building your navigation, heading, and footers around this section.

<div class="container">

2. Next, we have a div element with the id of title-first and a class of expandContainer. I'm using this class to control how this element will be positioned on the page. The id of this element will be used by jQuery to grab the id and it's child div element later on in this tutorial.

<div id="title-first" class="expandContainer">

3. Now we have a heading two element. This can be anything you like but at the very least, be consistent with what you use here.

<h2>Second Title</h2>

3. The second nested element is where most of the magic will be happening. This div element will contain all the content that we will be able to expand or collapse. Here, the id must be consistent in it's naming scheme but unique per instance. The class for this element handles the opacity and height of this block which we'll animate using jQuery shortly.

<div id="content1" class="expandContent">

4. Inside this block, you can pretty much put whatever you like in there. In this tutorial, I'm using a simple paragraph element with a link.

<p><a href="#">Link to content 1</a> is all about stuff inside the first container</p>

From there, make sure you closed off all your opening elements with the proper closing tags.
Finally, I copied this first block id'ed as title-first two more times and made each id unique within the first div (expandContainer class), the nested div (expandContent class), and the content unique as well. 

Now lets get to the magical part of this tutorial: JavaScript and jQuery!

JavaScript and jQuery

1. First off, let's declare some variables we are going to use:

var openClosedArray = [];
var contentArray = [];
var $contentLength = $("div[id^='content']").length;
var $id = "";
var $child = "";

The openCloseArray will store the display state of the nested div of the title id'd elements. The contentArray stores the name of the content div elements. $contentLength is used to store the number of div elements that start with the id of 'content'. We'll use this in a for and while loop later on. The $id variable will store the id of the clicked div element. Finally, the $child variable stores the id of the nested div of $id.

2. To make this container system flexible to account for any number of title id'd div elements, we are going to collect the ids of this div elements and set another array to handle the display states of each collected div element.

for (var i = 0; i < $contentLength; i++) {
    contentArray.push($("div[id^='content']:eq(" + i + ")").attr("id"));
    openClosedArray.push(false);
}
The for loop looks for all div elements that contain an id that starts with content and the loop will end once we get to the last element with that matching id. Inside the loop, we push each id we encounter into the contentArray array. We'll use this array to match the clicked on div element and take appropriate actions later. The final piece of this loop is the openClosedArray. This array receives the value of false for each time we encountered the matching id on 'content'. This array handles the display state of each content block.

3. We've captured the necessary element ids and are ready to start the main interactivity of this tutorial: expanding and collapsing. We'll use the .click method from jQuery to initiate the interaction. Let's just grab any element that starts with the id of 'title-':

$("div[id^='title-']").click(function() { ... });

4. Inside the function of this click method, we need to get the ids of the clicked div element and it's nested div:

$id = $(this).attr("id");
$child = $("#" + $id + " div").attr("id");

For $id, we use this which just passes the clicked element and then the attribute method with the argument of "id" to assign the id of the clicked div element. With $id stored, we are now ready to get the id of the nested div element and pass it to the variable $child. In the second line here, again, we use a CSS selector technique to get the nested div element of $id and then the attribute method to grab it's id.
One could argue (among many other ideas presented in this tutorial) that these two lines could be combined into one. One could but I like writing this out for debugging purposes and expandability reasons.

5. A while loop is used next to allow the script to search through the contentArray array and find a match for the clicked div element's id.

var loop = 0;
while (loop < $contentLength) {
    ....
    loop++;
}

It's important to note here that the initialization of the variable loop needs to be inside the click method. If it's not, our while loop will never end.

6. Inside the while, we use an if condition to compare the entry of the contentArray array to the $child variable.

if (contentArray[loop] == $child) {
    ....
    break;
}

We use the break statement to escape the while loop ounce we found our match. There's no reason to keep on looping through the array once we found our match.

7. The final (and big) step: toggling the display state of the $child:

if (!openClosedArray[loop]) {
    $("#" + $child).animate({height: "25px"},250).animate({opacity: "1"},250);
    openClosedArray[loop] = true;
} else {
    $("#" + $child).animate({opacity: "0"},250).animate({height: "0px"},250);
    openClosedArray[loop] = false;
}

The conditional if statement of !openClosedArray[loop], is looking to see if the value in this location of the array is false or true. If it not true, then it assumes that the user has clicked on the parent div element and wishes for the element to expand. The next line uses the animate method in a two-step process. You could combine the two animate methods into one but I rather like the $child element's opening up and the content fading in over the course of a half second total.

$("#" + $child).animate({height: "25px"},250).animate({opacity: "1"},250);
  
To get the toggling behavior, the next line is necessary to "flip" the state of the current position of the openClosedArray array.

openClosedArray[loop] = true;

If the conditional statement is true, then the script assumes the user means to close the expanded $child element runs another two animate methods to close up the div element.

$("#" + $child).animate({opacity: "0"},250).animate({height: "0px"},250);

Finally, we need to flip the openClosedArray array to the opposite state so the user can toggle it again if so desired.

openClosedArray[loop] = false;

There you have it! At this point, you should give your script a test run. Once you've had a chance to play with the script, try adding another content block just to make sure everything works fine:

<div id="title-fourth" class="expandContainer">
    <h2>Fourth Title</h2> 
    <div id="content4" class="expandContent">
        <p><a href="#">Link to content 4</a> is all about stuff inside the fourth container</p>
    </div> 
</div>

If all goes well, this new content block should work just fine. If you have any questions, you know know where to find me! To wrap things up, here is the entire document for those "TL;DR" typesout there.
Good luck and enjoy!

<!DOCTYPE html>
<html lang="en-US">
    <title>Expand System</title>
    <style type="text/css">
        h2 {
            color:#00C;
            cursor: pointer;
        }
        h2:hover {
            color:#00F;
            cursor: pointer;
        }
        div.expandContainer {
            display: table-row;
            overflow: hidden;
        }
        div.expandContent {
            height: 0px;
            opacity: 0;
            padding: 0 5px;
        }
        div.container {padding: 5px;}
    </style>
    <script src="jquery-1.11.0.min.js"></script>
    </head>
    <body>
        <div class="container">
            <div id="title-first" class="expandContainer">
                <h2>First Title</h2>
                <div id="content1" class="expandContent">
                    <p><a href="#">Link to content 1</a> is all about stuff inside the first container</p>
                </div>
            </div>
            <div id="title-second" class="expandContainer">
                <h2>Second Title</h2>
                <div id="content2" class="expandContent">
                    <p><a href="#">Link to content 2</a> is all about stuff inside the second container</p>
                </div>
            </div>
            <div id="title-third" class="expandContainer">
                <h2>Third Title</h2>
                <div id="content3" class="expandContent">
                    <p><a href="#">Link to content 3</a> is all about stuff inside the third container</p>
                </div>
            </div>
        </div>
    </body>
    <script type="text/javascript">
        var openClosedArray = []; // array to store the display state of the $child div
        var contentArray = []; // array to store the names of content div elements
        var $contentLength = $("div[id^='content']").length; // store length of expandContent classed div elements
        var $id = ""; // store the id of the clicked div element
        var $child = ""; // store the id of the nested div of $id
     
        for (var i = 0; i < $contentLength; i++) { // initialize display state of all div#content elements
            contentArray.push($("div[id^='content']:eq(" + i + ")").attr("id")); //get all ids of content div elements
            openClosedArray.push(false); // set all values of array to false (hide content by default)
        }
      
        $("div[id^='title-']").click(function() {
            $id = $(this).attr("id"); // get id of clicked div element that starts with an id of 'title-'
            $child = $("#" + $id + " div").attr("id"); // get id of $id's nested div. Note: this may not hold up if more div elements are added to this nest div structure
          
            var loop = 0;
            while (loop < $contentLength) {
                if (contentArray[loop] == $child) { // match clicked child div to id of stored array
                    if (!openClosedArray[loop]) { // if a match is found, check to see if the value is true or false; if true, display the $child div
                        $("#" + $child).animate({height: "25px"},250).animate({opacity: "1"},250);
                        openClosedArray[loop] = true;
                    } else { // if false, hide the $child div
                        $("#" + $child).animate({opacity: "0"},250).animate({height: "0px"},250);
                        openClosedArray[loop] = false;
                    }
                    break;
                }
                loop++;
            }
        });
    </script>
</html>

Developer's Note: This tutorial was successfully tested on Chrome 33, Firefox 17, and IE 11. There is a slight "hiccup" in the expanding and collapsing animation in IE 11.

Labels: , , , ,

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home