Learning jQuery, Day 5 : Enhanced Tabs
Today, let's look at how we can use the tabs that come with the jQuery UI library. I use these for most of the administrative back end applications I write. The jQuery tabs are great, but they lack some enhanced functionality that I need, so we'll look at how some simple JavaScript can provide things like closing tabs and ensuring that we don't have duplicate tabs.
Let's start with a simple page. On it, I want to display a menu. When any menu item is clicked, I want to load the contents of a separate page into a unique tab. You can see a demo of this here.
Here's my "index.cfm" page:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>jQuery Tabs Page</title>
</head>
<body>
<ul id="main_nav" class="lavaLampNoImage">
<li alt="Home" class="current"><a href="#">Home</a></li>
<li alt="Services"><a href="#">Services</a></li>
<li alt="Projects"><a href="#">Projects</a></li>
</ul>
<div id="tabs">
<ul id="tabsUl">
</ul>
</div>
</body>
<cfinclude template="index.pgm">
</html>
I've included "index.pgm" at the bottom of my page. Here's that code:
<link rel="stylesheet" href="/lavalamp-0.2.0/lavalamp_test.css" type="text/css" media="screen">
<style>
.tab_delete:hover{
cursor: pointer;
}
</style>
<script type="text/javascript" src="/jquery-ui-1.7.1.custom/js/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="/jquery-ui-1.7.1.custom/js/jquery-ui-1.7.1.custom.min.js"></script>
<script type="text/javascript" src="halogen.tabs.js"></script>
<script type="text/javascript" src="hash.js"></script>
<script type="text/javascript" src="/lavalamp-0.2.0/jquery.easing.min.js"></script>
<script type="text/javascript" src="/lavalamp-0.2.0/jquery.lavalamp.min.js"></script>
<script type="text/javascript">
$(function() {
$('#main_nav').lavaLamp({
fx: 'backout',
speed: 700,
click: function(event, menuItem) {
return true;
}
});
tabs = new Tabs($('#tabs'));
$('ul#main_nav li').click(function(){
var tabHash = jsHash($(this).attr('alt'));
tabs.tab($(this).attr('alt'), tabHash);
$('#' + tabHash).load($(this).attr('alt') + '.cfm');
});
});
</script>
I could, of course, have just included this code on my index.cfm file, but as I explained on the blog entry, "Form Processing, the OO Way : The Movie!", there are some advantages to having a separate code/processing page:
- My HTML page ("index.cfm") is very clean and is ideal for passing to a designer for applying styles
- If I wish to reuse a page in a different context, I can swap out the programming page to provide different behavior to the same HTML page. (I find this useful surprisingly often.)
In "index.pgm", I've included some stylesheets and some jQuery-related JavaScript scripts. I've also included two files: "halogen.tabs.js" and "hash.js". These will be used to provide the extra functionality to the jQuery tabs I need.
In the open "script" tag, I begin using the "$(function(){}" call. This is shorthand for the more typical "$(document).ready(){}" code. It tells jQuery to run a function once the document is ready for processing -- similar to "body.onLoad" but a bit faster.
In that "ready" function, I set up a jQuery plugin, "LavaLamp", that provides a little eye candy to my menu. The next call, "tabs = new Tabs($('#tabs'));" creates an instance of a "Tabs" class (inside my "halogen.tabs.js"), passing a jQuery object that wraps a DOM element. That DOM element comes from "index.cfm":
<ul id="tabsUl">
</ul>
</div>
As you can see, it's a div with a nested "ul" element, but there's nothing there to display. (We'll handle that with Ajax via jQuery shortly.) By instantiating the "Tabs" JavaScript class inside halogen.tabs.js, I've both enabled jQuery tabs and assigned additional functionality to them. We'll see this shortly, as well.
Back to "index.pgm", the last bit of code tells jQuery that when an "li" element within a with a "ul" element with an id of "main_nav" is clicked, the following should occur:
- Create a unique hash from the value of the "alt" tag of the clicked "li" element. This will be used by "halogen.tabs.js" to ensure that we don't have duplicate tabs.
- Call the "tab" method in our "tabs" JavaScript object, passing it both the title of the tab (the value of the "alt" attribute in this case), and the unique hash. Within the "tab" method, I check to see if a tab with an id of "tabHash" exists. If it does, I select that tab. If no such existing tab was found, a new tab is created with the given title and unique hash.
- Finally, assured that a tab exists with an id of the value of "tabHash", I load a file into that tab using the jQuery "load" method, passing it the appropriate file to load.
I'll show you the contents of "halogen.tabs.js", but it might be best to just treat this as magic code that will handle tabs for you, but for the curious, here it is:
Copyright 2009 by Hal Helms
hal@halhelms.com
941.716.6909
You may use this code for your projects, but please do not...
1. remove this comment, or
2. sell this stand-alone code. (You may incorporate it into your commercial projects freely.)
*/
function Tabs(jQuerySelector){
$(jQuerySelector).tabs();
this.bag = new Array();
this.add = function(title, uniqueHash){
var deleteString = ' <a href="#"><img border="0" class="tab_delete" src="delete-img.png" onclick="tabs.remove(';
deleteString += "'";
deleteString += uniqueHash;
deleteString += "'";
deleteString += ')"></a>';
$(jQuerySelector).tabs(
'add',
'#' + uniqueHash,
title + deleteString);
$(jQuerySelector).tabs('select', this.bag.length);
}
this.tab = function(title, uniqueHash){
for (i=0; i<this.bag.length; i++){
if (this.bag[i].uniqueHash == uniqueHash){
$(jQuerySelector).tabs('select', i);
return;
}
}
this.add(title, uniqueHash);
var str = {};
str.title = title;
str.uniqueHash = uniqueHash;
str.idx = this.bag.length;
this.bag.push(str);
return true;
}
this.remove = function(uniqueHash){
for (i=0; i<this.bag.length; i++){
if (this.bag[i].uniqueHash == uniqueHash){
this.bag.splice(i,1);
$(jQuerySelector).tabs('remove', i);
return;
}
}
}
}
In the video links below, I've done a walk-through of the code. Sometimes a video can help clarify complexity.


From the YSlow docs:
Put Scripts at the Bottom
tag: javascript
The problem caused by scripts is that they block parallel downloads. The HTTP/1.1 specification suggests that browsers download no more than two components in parallel per hostname. If you serve your images from multiple hostnames, you can get more than two downloads to occur in parallel. While a script is downloading, however, the browser won't start any other downloads, even on different hostnames.
Scripts should be at the bottom, CSS should be at the top. If the css is at the top, it allows progressive rendering.
http://developer.yahoo.com/performance/rules.html#...
Great tutorial otherwise.
;-)
And what's my upside anyway?
If your script is at the bottom of the page, what else is left to be rendered on the dom? You should be able to play with all the elements before the script even if the ondomready event hasn't fired. Developers have been sticking JS at the bottom long before all these dom ready events were around.
The only time i can see needing the dom ready event is when you have scripts that depend on each other at the bottom of the page.
I was actually reading a good post on another blog about the document ready function that says it actually can hurt to use it.
http://www.artzstudio.com/2009/04/jquery-performan...
I see the author's point, but I'm still going to use the document ready function if I see a need for it.
here's an example, and this is only something I can guess at: what if there's an event that fires off when </html> is found? (presumably when EOF is encountered the event fires too unless the browser is in STRICT mode). And that's just one I can think of. Note that I'm not arguing against putting scripts at the bottom; I am arguing against what seems to be your suggest of avoiding the document.ready()
method -- that it somehow is not needed if scripts are at the bottom.
Let me turn the question around on you: what does one gain by not using the document.ready() call? I'm supposing you're gonna say X milliseconds of loadtime, cuz I'm sure what else there is to gain except time.
I realize that x number of milliseconds is a small gain for this example, but what about if you had to wait for a dom load event when there is a long blocking script on the page? The google maps scripts come to mind, they seem to take forever to finish loading, which delays the dom load event.
I can see your point about using it anyway because it does provide you with many benefits (previous errors don't stop script from loading, you can put the code anywhere you want, backed by and tested by jQuery team, etc).
If I wanted the on-the-fly tabs to be loaded from a list of links on an inital, permanant tab, could that be accomplished? In my experiments, a permanent tab as such vanishes when clicking on one of the on-the-fly tags.
Any help appreciated.
I was wondering if you have come across an issue with having a <cfinclude> inside of the coldfusion page that is ajax loaded by jquery tabs?
The page loads fine if all it has is vanilla coldfusion code. But, I try to cfinclude other coldfusion pages "modules" inside it then I get a 500 - template not found error.
Just curious if there something I am doing something wrong or this is a known boo-boo.
Thank you.