Flexible, Dynamically-Expanding Columns with Absolute Positioning

April 22, 2010

April 22, 2010
 
To use this tutorial, you will need Prototype.js (v1.6) and a good working knowledge of javascript, CSS, and XHTML. This tutorial assumes you are an intermediate-to-advanced javascript programmer and understand the basics of programming and code design. I will keep the technical jargon to a minimum, but some concepts are just too basic to be covered in great detail here.
 
All of the files you will need are in this self-contained ZIP archive:
 Download this article’s source code here.

 

 
Question… 
How many of you out there have wanted multiple columns of content on web pages without all of the required housekeeping drudgery to make sure all the content gets displayed properly and nothing gets clipped, overlaps or gets shoved around by other elements? And without the fear of getting a call from your client, telling you that the site does not look good in browser X? From the search results I came up with, it would seem that there are a LOT of others like me out there who are looking for a simple solution that does not required executing acres of processor-intensive code. If your page is torturously-slow, what good is pretty content?
 
It has been extremely frustrating finding something that is flexible with a minimum of customization and that works on all browsers. Of course, I am not talking about supporting legacy browser versions, like Internet Explorer 6 and older… that is a COMPLETELY different beast with razor teeth and slathering jaws waiting to clamp down on your unsuspecting ankles. So, for the purposes of clarity and simplicity, I am going to focus only on Safari 4 (mac) / 4 (pc), Opera 9 (mac) / 10 (pc), Chrome beta (mac) / 4 (pc), Firefox 3 (mac) / 3(pc), and Internet Explorer 7 (pc) / 8 (pc).
 
So, Why Bother? 
Good question! Couldn’t this be done using pure CSS? Theoretically it is possible to create a wrapper DIV, and then simply insert column DIV’s into the wrapper. Then, you would use the “display:inline-block” style to stack them left-to-right, or you could use the style “float:left” to take it out of the layout-stream. You could also assign the style “height:auto” to each column to have them automatically expand or contract the column DIV height. Well, theoretically, if you did not want any padding, or margins, or extra spacing around your columns this would seem like the obvious path to success… however, this is not an ideal world. Because of browser differences and flat-out incompatibilities, it will not work this easily. For example, if you used the float method, then because it is not a part of the document-flow, it will not adjust the height f the parent DIV that it is contained in: content will begin to spill out of the bottom of your wrapper DIV, ruining you nice, organized layout. As for the display method, it is lacking since the tops of the column DIV’s will tend to shift up or down depending on the height of its neighboring DIV… so many issues!
 
Trust me, I have tried just about every combination of CSS styles imaginable. Sometimes they worked great in 2-out-of-3 browsers… but then almost is never going to be good enough when it comes to websites, is it? That is where Javascript comes in. With it, we can create a robust solution that ALWAYS works and gives us predictable results every time.
 
Also, most of the current solutions out there only resize things at the time of the page load. This solution updates all the time, since there are times when you want a more expandable-contractable page design.
 

What This Tutorial IS NOT… 
This tutorial is meant to be a starting point only. All code has been commented inside the Javascript and XHTML, and CSS. You may copy-paste this code and use it for any purpose commercial, or private, as long as you are not selling the code itself and it is an integrated part of an actual webpage project not intended to solicit sales of this code.
 

Let’s Get Started 
To begin with, let’s introduce the technologies we will be using to achieve these automated, absolutely positioned columns of content. I decided to do everything in as few technologies as possible and using things that would be updated and open-source for a long time to come. I settled on Prototype.js for my cross-browser support, and CSS/XHTML for everything else. Prototype.js is a javascript-expanding system which is widely used, fully supported, and is ALWAYS being extended by others and updated. So, there is a pretty good chance that it will be around for years. The other nice thing about Prototype.js is that the code you need for your pages will reside on the same webspace as your site, and is not retrieved remotely, so if your page works now, it should work until your webhosting provider makes enough changes in their webserver configuration that it breaks your site. Besides, the average life-span of a website is typically 3-5 years. Anything beyond that is an amazing, unexpected bonus.
 

Prototype.js extends Javascript quite well, and it allows me to write smaller, faster code that is rather cross-browser compatible. So, I created my functions in an external javascript file and simply import them at load-time for use in my page.
 

Our Logic Flow 
What I needed was a container (wrapper) DIV that would hold multiple columns (DIV’s). Each wrapper-enclosed DIV would become a vertical column of content that would dynamically expand or contract its height according to how much content was enclosed. As the columns approached or receded from the bottom of the wrapper DIV, it would enlarge or reduce the height of the wrapper DIV so that our longest column maintained the same distance from the actual bottom of the wrapper DIV. Also, the page needs to be able to update itself upon load and anytime that a column changes height. Oh, yeah, and everything needs to be positioned absolutely so I could control the layout exactly the way I wanted it. I also wanted header and footer elements that could act as either visual spacers, or as holders for page-cap graphics. A lot of times, when I create a “page” of content within a web page, I like to give it a visual top and bottom so that it stands out more from the rest of the content on the page.
 

How We’ll do It 
We will accomplish self-updating columns by using an event listener for any click on the document. This will execute a function called “pageSnap” which will adjust the page_wrapper DIV height in relation to its contained column DIV’s. Also, instead of calling each column of content by name, we will simply create an array that holds every element of the class “page_column”. This will come in very handy later when you want to make your layout a 3, 4, 5… column layout. Since the columns are called by their class and not by their id, you can name the actual column DIV elements anything you like. They just need to be assigned to the class “page_column” to be included in the self-updating process of the wrapper DIV.
 

Our Page Structure 
Below is a simple diagram of how our DIV elements are structured. A copy of this diagram is included in the source code for this project. As you can see, all of the individual DIV elements that make up our page of content are contained in a single wrapper called “outer_wrapper”. While this wrapper is not necessary, it does gives us the flexibility to take the entire chunk of elements and simply drop them into a new page design. By enclosing all the elements together, it keeps them from spilling out into other page elements and interacting in an undesirable way.
 
The names of each element also correspond to that element’s CSS class definition. Since class and id do not share the same namespace, your browser should handle these just fine. If you do run into some future, unforeseen conflict, you can always change the class names in the CSS, just make sure you change them in the imported CSS file as well. I have been using duplicate class and id names for a while and have yet to have them conflict… but that does not mean it will stay that way forever.
 

Let’s Dissect Our Code 

Click here to see a working version of this example code.
 
Click here to download the source code.
 
Okay, so now you have the basic logic, the “why should I bother”, and the overall element structure down, so let’s move on to dissect our code block-by-block. But, first, let me tell you that I am only going to go over the relevant code blocks. I refrain from describing all the code in the Javascript, CSS, and XHTML files since they have all been thoroughly commented.
 


document.observe("dom:loaded", startup);

 1. This block of code assigns an event listener for the DOM event “loaded”. Since this fires before the onLoad event on the body element, it makes a great “pre-startup” startup handler. Just be sure to practice good-housekeeping and remove the listener after you no longer need it.
 

var pageWrapper, footer;
 
2. Here we add a couple of variables to the DOM for later use. We do not want to assign them to their respective page elements yet, because those have not been registered with the DOM yet and will simply return an undefined element, which will break the page and nothing will resize properly, or at all. Now, we do not have to create an instance of these variable names here, but it is good practice to do it this way, in case we want to add other functions that might use these variables. We could avoid globals altogether by placing our variables as properties on the element itself, but that is another article altogether. For now, we will keep things as simple as possible.
 

function startup()
{
// turn OFF the event listener?s
document.stopObserving("dom:loaded", startup);

// [ OPTIONAL CODE BLOCK ]
// these are OPTIONAL and should be removed when you create your own layout
// they are only here to allow you to shrink and grow the test columns to see how the page functions.
// assign some listeners for our shrink/grow buttons
$('clickable_1').observe( 'click', function(myEvent) { stretchElement("20",myEvent) } );
$('clickable_2').observe( 'click', function(myEvent) { stretchElement("-20",myEvent) } );
$('clickable_3').observe( 'click', function(myEvent) { stretchElement("20",myEvent) } );
$('clickable_4').observe( 'click', function(myEvent) { stretchElement("-20",myEvent) } );
// [ /OPTIONAL CODE BLOCK ]

// get a handle for the page_wrapper main div holding your columns
// you could name your page_wrapper anything, just make sure you change the name here as well
pageWrapper = $('page_wrapper');

// assign a height offset value for out footer element. If it does not exist, it will return a zero height
try {
footer = $('page_footer').getHeight();
} catch(e) { footer = 0 }

// set up a listener for a click on the document since we do not know which elements will expand or contract out layout height
// we need to check every time for changes in the height of the pageWrapper DIV
document.observe( 'click', pageSnap );

// DON'T FORGET! You can update the page_wrapper height any time simply by calling this function.
pageSnap();
}

 
3. This is the function that will run AFTER the page has loaded and registered all the page elements with the DOM. Now, we can assign our elements to their respective variables and thereby get a valid handle for later use. You will notice that there is a stopObserve method called on the document object. This is where we stop listening for the “DOM:loaded” event.
 

pageWrapper = $('page_wrapper');
 
4. Here, we assign our “page_wrapper” element to the global variable “pageWrapper”. This will give us access to that element without having to change the occurance of the element name more than once.
 

try {
footer = $('page_footer').getHeight();
} catch(e) { footer = 0 }

 
5. There are times that you may not want a footer element. You cold simply assign the element a height of zero, but for the sake of robust code, I wanted something that would allow me to remove the “page_footer” DIV altogether without breaking my page. So, if the element does not exist, it simply ignores the error and assign a height value of 0 to the variable “footer”.
 

document.observe( 'click', pageSnap );
 
6. Here is where we now add an event listener to the document element. This allows us to automatically check our “page_wrapper” height without having to call it from every function that might resize our “page_column” DIV elements due to a user click on some size-changeing UI element. You could just as easily assign this listener to the “page_wrapper” element instead of the “document” element. But, for the vast majority of purposes this works best.
 

pageSnap();
 
7. Once the page loads, our “page_column” DIV’s may have already changed size due to font-size differences among browsers and font families. So, it is always a good idea to execute this function to make sure everything is adjusted right away.
 

function pageSnap()
{

// get a the object that was acted upon to get it to this function
var target = pageWrapper;

// calculate a starting prevHeight (previous height) of the page_wrapper DIV
var prevHeight = 0;//target.getHeight() - footer

// this creates an array of all child elements in the target parent (page_wrapper) that are of the class "page_column"
var targetKids = $( target ).select('[class="page_column"]');

// go through all page_column elements and adjust the page_wrapper DIV height
for(var index=0;index prevHeight )
{
target.setStyle({ height:String( (height + footer) + "px" ) });
}
// save our current height as the prevHeight variable for reference on the next "page_content" column
prevHeight = height;
}
}

 
8. This is the function that will make the actual column adjustments. For the sake of simplicity, I have assigned the “pageWrapper” handle to a new variable called “target”. Of course, you do not need to do this, but I like to reassign our global to a local variable so that there is only a single instance of my global in my function definition. Also, if I wanted to have this function work on multiple “page_wrapper” element in a single webpage, I would just have to assign our “click” listener to each “page_wrapper” instead of the document, and then assign the “wrapper” variable to the “this” variable which would give us a handle to whatever element called the function without having to track all the different names of a the different “page_wrapper” DIV elements.
 

var prevHeight = 0;
 
9. Here, we want to create a variable to hold the previous height of the last “page_content” column whose height we compared to the height of the “page_wrapper” element. I assign it to zero, simply because of we try to add it to another integer and then assign it the height style property of an element, it will break our page in some browsers. By setting its inital value to 0, it helps to guarantee that we do not get some erroneous starting value.
 

var targetKids = $( target ).select('[class="page_column"]');
 
10. Here is probably the trickiest part of our code. What the select method allows us to do is collect all the occurances of the “page_content” class into a single array that we can iterate through and compare each element individually to the height of our “page_wrapper” DIV.
 

for(var index=0;index prevHeight )
{
target.setStyle({ height:String( (height + footer) + "px" ) });
}
// save our current height as the prevHeight variable for reference on the next "page_content" column
prevHeight = height;
}

 
11. Here we create a loop that will iterate through all of the elements in our “targetKids” array. Now, we could have done this with an each method and assigned our function to each of the elements themselves in the array, but for the sake of simplicity, I decide to just use the tried-and-true for-loop. Unless you have many, many elements to be compared, you should not notice any really appreciable speed difference.
 

var kid = $( targetKids[index] );
 
12. Here we assign our first element in the array to a shorter variable name. There is just too much room for error to have to type, retype, or even copy-paste the long name of “$( targetKids[index] )” every time we need to reference it. The shorter your variable names the less chance of typos.
 
 

var top = parseFloat( kid.getStyle( 'top' ) );
var height = kid.getHeight();

 
13. Now, let’s get a floating-point version of the values stored in our style property “top” and “height” on the “kid” element.
 

if( height + top > prevHeight )
{
target.setStyle({ height:String( (height + footer) + "px" ) });
}

 
14. Now, let’s add the height of our “page_content” column to its “top” style value to get its actual height inside the “page_wrapper”. If our content is now taller than the holding DIV element, we adjust the height of the holding DIV to the same as the height of our “kid” element PLUS the height of our “page_footer” element. This will make sure our holding DIV stays taller than or “page_content” DIV.
 

prevHeight = height;
 
15. So, all we need to do now, is save the current height value for comparison against the height of the next element. Using this value gives us a simple way to make sure that if the next column is taller than the last, we need to increase our “page_wrapper” height. Otherwise, if the next column is shorter, we can simply ignore it and keep the “page_wrapper” height unchanged. This way, we do not need to compare every column to every other column every time we look at each column height.
 

16. Also, make sure that if you want to move your columns, or add columns, you duplicate the “page_column” DIV element, give it a unique instance “id” and override any “left” or “top” properties using an inline style definition. The default position for all columns in the CSS class definition for page columns is at top:0, left:0.
 


 

Thanks 
That is about it. Pretty simple, actually. But not necessarily obvious. So, I hope this tutorial has helped.
 
Download this article’s source code here.

Advertisements