Javascriptery: Tabbed forms

Forms are perhaps the bane of web development for me; you can't get them to look good, you can't find a foolproof way to make them act well and lets not even start of trying to get them into a pacified state, free from the dangers of user input (surprise ending: form input will never be completely trustworthy). A lot of sites would appear to have aesthetically pleasing forms, however this is a careful ruse by them as they sidestep the problem of forms by having only one or two of them, and then they usually only have a few fields. The monstrosities I am required to deal with almost daily are things of grotesque beauty, veritable Rube Goldberg machines of complexity.

The long and the short of this diversion into why forms are evil (please, end my suffering quickly XForms) is that to get a form looking good, you have to spend a long time fiddling with things. Enough of this banter anyway, my fiddling with JavaScript (like the dirty little bastard child of C and Perl it is) produced a way of creating a tabbed form that defaults to a standard single form if a user prefers to use NoScript or an antiquated browser of yore.

So the following markup:

<form id="theForm" method="post">
    <fieldset>
        <legend>First tab</legend>
        <ol>
            <li><label for="formone">One</label>
                <input id="formone" name="one" type="text" /></li>
            <li><label for="formtwo">Two</label>
                <input id="formtwo" name="two" type="text" /></li>
            <li><label for="formthree">Three</label>
                <input id="formthree" name="three" type="text" /></li>
        </ol>
    </fieldset>
    <fieldset>
        <legend>Second tab</legend>
        <ol>
            <li><label for="formfour">Four</label>
                <input id="formfour" name="four" type="text" /></li>
            <li><label for="formfive">Five</label>
                <input id="formfive" name="five" type="text" /></li>
            <li><label for="formsix">Six</label>
                <input id="formsix" name="six" type="text" /></li>
        </ol>
    </fieldset>
    <fieldset>
        <legend>Third tab</legend>
        <ol>
            <li><label for="formseven">Seven</label>
                <input id="formseven" name="seven" type="text" /></li>
            <li><label for="formeight">Eight</label>
                <input id="formeight" name="eight" type="text" /></li>
            <li><label for="formnine">Nine</label>
                <input id="formnine" name="nine" type="text" /></li>
        </ol>
    </fieldset>
</form>

The default will be to display the first fieldset, with links in a list to display the other two. A trivial form like this certainly doesn't require a tabbed layout, but a monstrosity that contains 27 input fields (some multiple choice) could do with a little information management when displayed to the user. The general markup is what I've settled on for the majority of my forms and is based almost entirely on Nick Rigby's article on ALA but the styling isn't what's important here.

For this project, like any I undertake with JavaScript, I'll be using the Prototype library (version 1.6 specifically for this snippet), this could be done without it with minimum fuss but Prototype is lovely so I usually already have it included.

The functionality of this project is pretty minimal, the building of a list of the available fieldsets lies at the core of it. When the script is invoked it will hide all but the first fieldset, build an unordered list of the fieldsets (taking the names from the <legend> elements) and then set up event listeners for that list to change the visible state of each fieldset.

First things first, set up the Javascript object and hiding of the fieldsets:

var tabbedForm = {
    init: function() {
        var formElem = $('theForm');
        if(formElem)
        {
            $A(formElem.getElementsByTagName('fieldset')).each(function(s, i) {
                var fieldsetId = s.identify();
                // hide all but the first
                if(i != 0)
                {
                    s.hide();
                }
            });
        }
    }
};

Nothing spectacular, uses the oft ignored index property of the each() function to scry when it's not the first in a list, there are plenty of other ways of achieving this. Next job is to build the list of available fieldsets and plop that into the document at some point, so augmenting the init() function:

init: function() {
    var formElem = $('theForm');
    if(formElem)
    {
        var listElem = document.createElement('ul');
        $A(formElem.getElementsByTagName('fieldset')).each(function(s, i) {
            var fieldsetId = s.identify();
            // hide all but the first
            if(i != 0)
            {
                s.hide();
            }

            var legendElem = s.down('legend');
            if(legendElem)
            {
                var listItemElem = document.createElement('li');
                var linkElem = document.createElement('a');
                linkElem.href = fieldsetId;
                linkElem.innerHTML = legendElem.innerHTML;
                Element.addClassName(linkElem, fieldsetId);
                Event.observe(linkElem, 'click', tabbedForm.tabClicked);

                listItemElem.appendChild(linkElem);
                listElem.appendChild(listItemElem);
            }
        });

        Element.insert(formElem, {before: listElem});
    }
}

An unordered list item is created, the for each fieldset, the <legend> element is nabbed and its value used as the title for each list item. Probably the only questionable part is making the link element point to the ID of the fieldset, this is just how I do things so that when a link is clicked, the ID is available. Other people I know put these sort of items within the Javascript object itself or in a classname or somesuch, whatever works for you; I don't have to worry about non-Javascript users clicking the links because the entire structure is generated rather than marked up. I drop the completed unordered list above the form element which fits with the "tab" metaphor we're aiming for.

The only remaining function is what happens when a link in the generated list is clicked which according to my event listener is called (cunningly enough), "tabClicked":

tabClicked: function(evt) {
    Event.stop(evt);
    var linkElem = Event.findElement(evt, 'a');
    var formElem = $('theForm');
    if(linkElem && formElem)
    {
        var idToShow = linkElem.href.substr(linkElem.href.lastIndexOf('/')+1);
        $A(formElem.getElementsByTagName('fieldset')).each(function(s) {
            if(s.identify() == idToShow)
            {
                s.show();
            }
            else
            {
                s.hide();
            }
        });
    }
}

After stopping the link click event from bubbling up any further it grabs the clicked link element (I find it best not to take for granted which element has been clicked and just do a "findElement" to make sure we're on the same page), pulls the ID from href attribute then iterates through the form's fieldsets to find the one it refers to.

At this point the scripting is completed and a barebones proof of concept can be seen. Obviously with no style it's not going to look like tabs, but with a little sliding-door tomfoolery, you'll be tabbed up in no time. At this point you'll likely want to expand on the functions above by dropping in some choice CSS classes, setting the active tab to "on" for appropriate styling and maybe even adding some other classes to let your stylesheet know things have been modified by a script (I find simply added a "scripted" class to the container element works wonders).

The beauty of this is it's accessible (the form still works 100% without scripting) and it prevents a user from seeing just what a mammoth form they may be completing (blood of your first born? yes please).

Respond to “Javascriptery: Tabbed forms”

Community rules:

  1. Keep it civil: no personal attacks, slurs, harassment, hate speech, or threats
  2. No spam: includes marketing, pyramid schemes, scams etc.
  3. Notify of any spoilers: even if it's for something the post isn't about
  4. Your response may be edited or removed: if your response was in good faith, you may be contacted via email explaining why

Your address will never be shared

The following HTML tags are allowed: <b> <strong> <i> <em> <a href>