Implementation
To begin with, the most neglectedand most intuitiveaspect of modern programming is intelligent encapsulation (object is the first O in OOP). To manage an application or a project, you must be able to rely on the predictable (if not always complete) functioning of the component parts. Search engine "tabs" are really links to distinctly generated pages rather than the clean UI function implied. The basis for our encapsulation will be a combination of <DIV> tags (even Netscape handles <DIV> better than <LAYER>), DHTML, and the JavaScript function object (see Listing 1):
Listing 1. Basis for the TabPanel object (full listing in TabPanel.js).
// Empty callback method function OnTabSelected(tabID) {} function TabPanel (myDiv) { // Properties this.tabFrame = myDiv; this.tabs = new Array(); this.panels = new Array(); // Standard function object methods this.paneStyle = returnPaneStyle; this.paneHTML = returnPaneHTML; // Methods specific to TabPanel this.add = createPane; this.setContents = setPaneContents; this.select = setTopPane; this.current = currentTopPane; function returnPaneStyle() {} function returnPaneHTML() {} function createPane(paneName) {} function setPaneContents(paneName, paneContents) {} function setTopPane(paneName) {} }
Here's the corresponding HTML segment:
<HEAD> <SCRIPT LANGUAGE="JAVASCRIPT" SRC="TabPanel.js"></SCRIPT> <title>Tab Panel Object Test Bed</title> <STYLE> #maindiv { position: absolute; top:40; left:20; height:300; width:300; clip: rect(0 300 300 0); background-color: black; } </STYLE> </HEAD> <BODY ONLOAD="init()"> <DIV ID="maindiv"></DIV> <SCRIPT LANGUAGE="JAVASCRIPT"> var testObj = new TabPanel("maindiv"); document.write(testObj.paneStyle()); function init() { if (document.all) { document.all["maindiv"].innerHTML = testObj.paneHTML(); } else { document.layers["maindiv"].document.open(); document.layers["maindiv"].document.write(testObj.paneHTML()); document.layers["maindiv"].document.close(); testObj.activate(); } } </SCRIPT>
NOTE
I'd like to take a moment to note a couple of subtle modifications required for this object to function in Netscape. These are typically demarcated in the else clause of an if (document.all) block. Needless to say, nothing complicates a clean object implementation like shooting at two targets. While the full breadth of these changes is beyond the scope of this article, most of them are covered in the full code listings. Try removing some of these blocks to see the differences in browser behavior.
Continuing with the real subject, the structure of JavaScript objects is remarkably simple. The functions enclosed in braces become accessible only to the other functions within the braces, unless exposed as members of an object's interface. If you choose to limit the items within the braces to the properties of the object and leave the methods as "global," they can still be members of the object's interface. Be careful, though, as JavaScript interpreters will only remember the most recently defined version of a functioneven if defined in separate filesif they're included on the same HTML page.
The primary reason to include a function in an object's interface is to allow for the action of member or "class" variables. By namingand callingfunctions and variables with the this reference, multiple instances of different objects can be maintained with their own set of variables retained in scope. In other words, the only functions that should remain as "floaters" are utility functions, where the only interest is in the return variable based on input parametersspecial string-parsing routines are good examples. The other important aspect to take away from this listing is that the containment of the object in a separate file is almost irrelevant. This is true except that the best way for JavaScript to be shared across HTML pages is through the use of the src parameter in a script tag. Combine this with the passing of the DIV id within the constructor andpresto!easy encapsulation and code reuse.
This leads me to my second area. Every software project manager wishes his or her group to possess one and only one of everything. Duplication of effort is the bane of a manager's happy, productive day. There are many ways to leverage these simple mechanisms to produce implementation-specific code from general objects. First, included within the language is the operator prototype. TabPanel.prototype.<new function> can extend and specialize the tab panel for some purpose. Second, instead of coding standards governing only syntax and formatting, a general tab panel could exist, with many people then overriding the setContents methods to tailor the panel to hold a specific type of data. Even the inclusion of the general TabPanel.js can be eliminated from the HTML by including it in the child object's file, in this format:
document.write("<SCRIPT language="javascript" src="TabPanel.js"></SCRIPT>");
As mentioned earlier, be careful, because object scope is not a bulletproof defense against carelessness. If two objects derived from TabPanel.js are on the same page and they both override setContents, I can't be held responsible for the results. Sometimes it will work out okay, sometimes not. It's precisely because browsers can be so unpredictable on seemingly identical code that good design is so important. Test every object independently on every platform required. The time spent up front is more than outweighed by the reduction of debug time and the "It must be the browser" whining. Ironically, of course, it's precisely because browsers do so much for the developer that most assume throwing random functions onto a page constitutes an acceptable application.
Finally, event-driven program flow is at the heart of almost any application that involves user interaction. It's also the most difficult aspect to implement reliablyagain, when a browser chooses to pass on the message you want to receive is often a mystery. It's also difficult to disable, as in the case of trying to keep the user from resizing a window. The problem is that all program executions in a relatively stateless connection take place in response to events (for example, the page finishes loading, the Submit button is pressed, or a link is clicked). Because there is technically no server activity that can result in a change to a Web page once the user has received it, client-side events to monitor and trap must be chosen wisely. Hacks such as timer-based execution are often necessary, but they should be avoided.
In this case, implementing general tab functionality is relatively straightforward. I'll stick to OnTabSelected(tabID) notification. This event will fire when the user changes tabs. As listed above, each tab added to the panel must have a unique identifier. This is important for other obvious reasons, but it also serves the purpose of navigation. In TabPanel.js, an empty prototype of the OnTabSelected(tabID) event should be listed. I say should only in case someone fails to remember to (or chooses not to) override it. In that instance, the tab panel would simply not act tab-like. Otherwise, the dreaded Object expected error would be shown anytime a user clicked on a tabwhen the HTML of each pane is constructed, in the tab section, the following is included:
<DIV id=[paneTitle] onmousedown=OnTabSelected([paneTitle])></DIV>
By passing the name of the tab as a parameter, the override in the HTML becomes very simple, but allows a great degree of flexibility. For example:
function OnTabSelected(tabID) { obj.select(tabID); }
Again, voilà. Not too challenging. What if you wanted to check whether a form element was properly filled in prior to allowing the user to continue? Just add a conditional execution:
if (e-mail input box text has a '@' and obj.current == "UserInfo") { obj.select(tabID); } else { alert("Please enter a valid e-mail address"); }
No need for a check on an ASP or CGI script at the server when the form is finally submitted. I think the three panels in the test object demonstrate sufficiently the variation of display that's possible.
Look past the evangelizing and the sheer usefulness of a TabPanel for a moment. What am I trying to say? It would be easy to quibble with my implementation of a TabPanel. Why are there no NextPanel/PreviousPanel buttons, for instance? It's ugly, for another (try using images for the tabs instead). The difference between this stripped-down version of my current attempt and my first attempt is truly striking. But that's exactly my point. Behaviors of your application can always be isolated and implemented as needed. Just because the Web is a different arena doesn't mean that the old rules don't apply. The best way to meet a deadline while exceeding customer (both internal and external) expectations is still to define pockets of functionality. It's still very difficult (if not impossible on many timelines) to create such clean behaviors as I've outlined, but even if those pockets don't lend themselves to nameable objects, being able to consolidate and insulate them in a clean and simple way will make you a better coderor at least give you a better handle on just what value your project is attempting to provide. It will allow for expanded functionality, better user experience, and almost certainly a shorter development cycle.