Creative Commons

2009-01-19 11:56:14

Adding "Tabs" To Framed Web Applications

If web developers have one major gripe (other than IE not conforming to standards) it has to be when they are tasked to create fat-client functionality in a web application. What do I mean by this? Imagine taking Microsoft Word 2007 and trying to make it usable in a web browser. It's not impossible, but it's close to impossible. Some would say it's improbable. That's just how the web works; it's stateless. You can't "remember" things like you can when you have an application installed locally on your machine. Well, I recently had to add "tabs" into a framed web application and have those tabs control other frames and be controlled by other frames. This is simple on a fat application, but not so simple in a web application. So let's see how we can make this work.
First, let's write the HTML for our tabs:
<div id="tabs" style="display:none;"> <div id="tabone" class="tab t0"> <a href="nolink" onclick="tabclick(this, '1');return false;">Tab 1</a> </div> <div id="tabtwo" class="tab t0"> <a href="nolink" onclick="tabclick(this, '1');return false;">Tab 2</a> </div> <div id="tabthree" class="tab t1"> <a href="nolink" onclick="tabclick(this, '1');return false;">Tab 3</a> </div> <div id="tabfour" class="tab t0"> <a href="nolink" onclick="tabclick(this, '1');return false;">Tab 4</a> </div> </div>
As you can see, we have a main DIV that encapsulates our tabs so that we can position them where we want by styling a single DIV instead of each individual tab. Inside of the main DIV we have one DIV per tab, each with it's own unique ID, which will come in handy later. From there, we just have our text surrounded by your standard A tag to make it a hyperlink. Before I go on, let me show you the CSS I used to make the tabs look decent:
div#tabs { position: absolute; bottom: 0; left: 7px; } div#tabs { float: left; height: 22px !important; height: 30px; width: 60px; background-color: #fff; margin-left: 3px; padding-top: 8px; text-align: center; } div#tabs div.t0 { background-color: #f0f0f0; border-bottom: 1px solid #c0c0c0; } div#tabs div.t0:hover { background-color: #fff; } div#tabs div.t1 { height: 23px !important; height: 31px; } div#tabs div.t1 a { color: #000; }
As you can see, I absolutely positioned the main DIV, but you can position it wherever you want. The style gets applied to each individual tab DIV. Then, I have two distinct CSS classes, t0 and t1, for an inactive tab and an active tab, respectively. I also have a class, t0:hover, so that upon mousing-over an inactive tab, the background changes to provide feedback to the user. Pretty simple stuff so far...
Let's go back to the HTML for a second, particularly the A tag, and talk about what we plan to do.
<a href="nolink" onclick="tabclick(this, '1');return false;">
If you noticed, I have the HREF set to something that doesn't exist, nolink. There are times when certain tabs will need to be inactive and not able to be clicked on. How do you prevent a hyperlink from doing what it was meant to do? This is where the next item comes into play. The A tag also has an ONCLICK javascript function. This function will do two things: change the CSS style of the tab to make the clicked tab active and all others inactive and follow the hyperlink of the tab if you allow it. The extremely important part here is the "return false;" after the javascript function. When you have this inside of an A tag, the hyperlink will not be followed. By default, javascript ONCLICK events get executed before A HREFs get followed, so by making the javascript return "false" we can halt code execution thus making the HREF not be followed. This is important because we don't want the HREF followed if it is "nolink" because that will result in a 404 error, and that's bad.
So now that you know what we want our A tag to do in plain English, let's look at the javascript code that makes it all happen.
function tabclick(e, follow) { var regex = /nolink/; if(!regex.test(e.href)) { var target, source; var x = parent.topFrame.document.getElementById('tabs').getElementsByTagName('DIV'); for (var i = 0; i < x.length; i++) { if(isString(e)) { target = x[i].id; source = e; } else { target = x[i]; source = e.parentNode; } if(target == source) { x[i].className = 'tab t1'; } else { x[i].className = 'tab t0'; } } if(follow != undefined) { parent.mainFrame.location = e.href; } } } function isString(str) { if(typeof str == 'string') { return true; } if(typeof str == 'object') { var criterion = str.constructor.toString().match(/string/i); return(criterion != null); } return false; }
Let's examine the tabclick function first. It takes two arguments, e and follow, which allow it to be called in two ways:
tabclick(this, '1');
The first method of calling the function is to be used directly from the A tag of the tab. In this case, the "e" javascript variable will be "this" passed from the HTML (the A DOM element) and the "follow" javascript variable will be "1," meaning we want to follow the HREF hyperlink. The second method of calling the function is to be used from other frames in the application and will only change which tab appears active. To use it this way, you simply pass a string of the tab's ID. The reason for this is that other frames in the application need to change which "tab" is showing in the main frame, so it needs to be able to change the tab without executing the HREF.
Let's walk through the code of both calling methods so you can see how the function reacts to both types of arguments. First, if it was called with two arguments, the first being "this," an object, and the second being "1," a string. After the function determines that the A (this) object's .href attribute does not contain "nolink" (I will show how to change this later), we move into a loop of every DIV inside of our main tab DIV (which will iterate four times because we have four tabs). During each iteration it will determine if the "e" argument is a string (source). If it is not a string, we assume that it's an object. So we set two variables; target will be the currently iterated tab DIV and source will be whatever tab DIV the user clicked. (Note: we needed to use .parentNode because "this" refers to the A tag, not the tab DIV that encapsulates it). Now that we know the target and source tab DIVs, we change the CSS class to make it appear active (t1) or inactive (t0). Finally, since we passed "1" for the "follow" variable, we fake the act of clicking a hyperlink by calling our destination frame's .href attribute. To the user, they think they've just clicked a normal hyperlink.
Now for the second way of calling the function, by only passing one argument, a string of the tab's ID. Remember, this method is only to be used to change what tab appears to be active, nothing else. The regex test for "nolink" will fail, which looks like a bug in the code but ultimately isn't, because a text string does not have an .href attribute. This means we enter the IF block. Since our argument is a string, we correctly set the target and source variables to be the correct DIVs. After that, it's the same code to change the CSS style to change the active tab. However, since we didn't pass a "follow" argument, it's "undefined," which means the code will not change our main frame's .href attribute. So, we have changed the active tab without executing the HREF.
So, this code is meant to work when the A tags actually have HREFs associated with them, however we made all of them have "nolink" as their HREF in the HTML. This is because depending on what content the user is looking at we will change where the tabs point. To do this, you simply have to make one javascript call from whatever frame needs to change the HREF of a tab:
parent.topFrame.document.getElementById('tabone').childNodes[0].href = '';
This code assumes that the link text of each tab is the first A tag in the DIV. If you use my example, it's the only A tag in the DIV, so you're fine.
So that's it. I know it looks complicated, but so is making web applications act like fat applications. I realize that most people will chastise me for using frames, however I begrudgingly gave in to the idea of frames after realizing what needed to be done couldn't be done with a single page and a fully CSS'd layout. I hope this is helpful to someone, if for nothing else than to give some insight to how you can modify DOM elements in other frames using javascript.


Post a comment!