After the prequel above, we can now get in to the nuts and bolts of a solution. There are a wide variety of approaches that can be taken when doing this, but this approach is aimed at being easily understandable and widely usable.
Further posts can cover other topics such as making things more easily configurable, and adjustments that can be made to allow several different tabbed sections to be on one page.
But for now - let’s get in to the code.
Code Wrapper
The development code that we’re using is contained within the following wrapper:
/*globals helper, microAjax */
(function (window, document) {
'use strict';
}(this, this.document));
JSLint or some other linter is useful to help weed out any significant issues in the code, so my code editor has been set up to automatically check for and and notify about any JavaScript issues. The global comment at the start is so that the automatic checking knows about them when it does its checks.
The function wrapper helps to prevent any functions within it from appearing in the global namespace. It’s best to keep the global namespace as clean as possible. Also, using the function wrapper gives us a later flexibility to develop that function as a single tabs object from which configuration can occur. Doing so can help to remove code smells such as magic numbers and strings, but that’s something that can be delved in to later on as further part of the project.
Helper functions
The class-handling functions and microAjax have already been covered in the previous post, but other helper functions are wantred here too. Because there are several helper functions that we want to use, it can help to groups them together under a consistant global object which we in this case will call helper. Most of the external functions we’ll be using can be placed in the helper object. It’s technically possible to put microAjax in there too, but for now we’ll keep that code as it comes from their website and we can put aside making changes to it for a later time.
The class-handling functions though can definately all go to the global helper object. Here’s how we do that.
Check if the helper object exists and if it doesn’t, create a new object for it.
class-handling.js
(function (window, document) {
window.helper = window.helper || {};
...
}(this, document));
Then, when we are defining the class-handling function to be used, we can add them to the helper object.
var helper = window.helper,
supportsClassList = !!document.body.classList;
if (supportsClassList) {
helper.hasClass = classListContains;
helper.addClass = classListAdd;
helper.removeClass = classListRemove;
} else {
helper.hasClass = hasClass;
helper.addClass = addClass;
helper.removeClass = removeClass;
}
The other useful functions that we have are too small to belong in their own script file, and yet are too general to be placed solely in the project that we’re working on. That in this case applies to the forEachElement and preventDefault functions.
We can create utils.js and place them in there,
utils.js
(function (window) {
'use strict';
// forEachElement and preventDefault functions defined here
// ...
window.helper = window.helper || {};
var helper = window.helper;
helper.forEachElement = forEachElement;
helper.preventDefault = preventDefault;
}(this));
forEachElement
The forEachElement function allows us to easily work with a NodeList collection of elements. We are advised that the [url=“https://developer.mozilla.org/en-US/docs/Web/API/NodeList#Why_can’t_I_use_forEach_or_map_on_a_NodeList.3F”]array forEach method is not available for nodeLists, and that extending the the DOM to add a custom method is dangerous with older versions of Web browsers. It is recommended instead to use a normal for loop to iterate over such collections of elements, so this forEachElement function can help us to do that with ease at the expense of being a global object.
I’ve kept the arguments for the callback function consistant with the arguments that are used with the forEach callback too, to help maintain a sense of consistancy.
// Due to there being no native forEach methods to handle
// iterating over elements, this forEachElement function
// makes it easy to do so using the recommended for loop
function forEachElement(els, callback) {
var thisArg,
el,
i;
for (i = 0; i < els.length; i += 1) {
el = els[i];
thisArg = el;
callback.call(thisArg, el, i, els);
}
}
preventDefault
Because preventing the default action on web browsers can be tricky when using advanced event regestration techniques in a cross-browser environment, this preventDefault function helps with some of the details that are involved.
// The typical returning false from an event handler is not effective when using addEventListener.
// Instead, control is achieved in W3C compatible browsers with evt.preventDefault() and evt.stopPropagation()
// and in Internet Explorer by setting to evt.returnValue = false and evt.cancelBubble = false
function preventDefault(evt) {
if (evt.preventDefault) {
evt.preventDefault();
}
evt.returnValue = false;
}
The tabs script
Due to JSLint rightfully expecting functions to be defined before they are called, this tends to result in smaller detail-oriented functions being higher up, and big-picture more interesting functions lower down, so I’ll go through the functions in reverse order starting from the bottom.
Attach tab handler
When someone clicks on a tab link we want our script to take action, so the first thing to do is to attach a click handler to each of those links. Another possible option could be to instead attach just one event handler to the #nav section which is useful to do when a large number of elements may be triggering events, but figuring out which element triggered what event can involve other complexities so we can stay away from that. Because there are just a few tab links to deal with and not 50 or 1000 tabs, attaching an event to each of them is in this case an effective and easy solution.
function init() {
var tabLinks = document.querySelectorAll('#tabs' + ' ' + '.nav a'),
tabClickHandler = function tabClickHandler(evt) {
evt = evt || window.event;
helper.preventDefault(evt);
handleTabClick(this);
};
helper.forEachElement(tabLinks, function (el) {
el.addEventListener('click', tabClickHandler, false);
});
tabLinks[0].click();
}
init();
Placing the code in an init function can be useful for us later on when it comes to extending the script, and the tabLinks[0].click(); line helps to provide a nice way to load first tab link.
Tab click handler
When someone clicks on one of the tab links, we don’t want the web browser to navigate to that link, so we have to tell the web browser to prevent the default behaviour for that link. After we’ve done that we can then go ahead and do our tab click actions.
var tabClickHandler = function tabClickHandler(evt) {
evt = evt || window.event;
preventDefault(evt);
handleTabClick(this);
};
Tab click
When someone clicks on a tab, we don’t want to do anything if it is already the active tab. If though it is not currently active, we want to fade out the old content and fade in the new content. Because the fading is done by using a setInterval method to change the class name, the updateAndLoadTabHandler callback function can easily access the tabs and file info thanks to being defined from within the handleTabClick function.
The updateAndLoadTabHandler is run after the content of a tab has faded out. It activates the new tab and load in the new tab content. It doesn’t need to be passed any variables either because it already knows them thanks to a nice feature called closure, where when a function is defined it retains knowledge of the environment from its parent function.
function handleTabClick(link) {
var tabLinks = document.querySelectorAll('#tabs' + ' ' + '.nav a'),
container = document.querySelector('#content'),
updateAndLoadTabHandler = function () {
var fileToLoad = link.href;
updateActiveTab(tabLinks, link);
microAjax(fileToLoad, function updateContent(response) {
container.innerHTML = response;
});
};
if (helper.hasClass(link, 'active')) {
return false;
}
fadeTransition(container, updateAndLoadTabHandler);
}
The updateAndLoadTabHandler calls two functions, updateActiveTab and microAjax, which has been recently covered in a previous post.
Update active tab
The updateActiveTab function loops through each tab removing the active class name from each of them, and then adds it back on to the new active tab.
// because tabLinks is a node list, using an ordinary for loop
// is best recommended for looping through a node list, so the
// forEachElement function helps us to easily achieve that.
function updateActiveTab(tabLinks, currentLink) {
helper.forEachElement(tabLinks, function (el) {
helper.removeClass(el, 'active');
});
helper.addClass(currentLink, 'active');
}
Fade tab
The fadeTransition function helps to simplify the task of fading out one set of content and then fading in the next. The callback function contains all that is needed to be known to change the tab and load in the next lot of content, so it’s just a matter of waiting for the fadeout to have finished before running the callback function and fading in the new content.
// The fadeout effect is given time to occur before the new tab is loaded
// thanks to a simple setTimeout delay
function fadeTransition(el, callback) {
var delay = 800;
fadeOut(el);
window.setTimeout(function () {
callback();
fadeIn(el);
}, delay);
}
It’s the CSS styling that determines how the fadeout occurs, and for how long, thanks to cross-browser transparancy support from CSS-Tricks and using CSS transitions.
/* IE opacity compatibility code thanks to
http://css-tricks.com/css-transparency-settings-for-all-broswers/
*/
#content.fade-out {
zoom: 1;
filter: alpha(opacity=0);
opacity: 0;
}
#content.fade-in {
zoom: 1;
filter: alpha(opacity=100);
opacity: 1;
}
/* vendor prefixes thanks to http://cssprefixer.appspot.com/ */
#content {
-moz-transition: opacity 0.5s ease-in;
-o-transition: opacity 0.5s ease-in;
-webkit-transition: opacity 0.5s ease-in;
transition: opacity 0.5s ease-in;
}
Fade tab
Finally, we fade the content by updating the class name and allowing CSS to perform the fading for us.
There are a few older web browsers that the fade transition won’t be supported on, but as it’s a non-vital feature, it’s okay for older web browsers to go without. A large amount of extra code would be required to provide older browsers with support for fading, which in this case is not worth the additional cost.
// The fading transition is handled by a CSS transition effect
function fadeOut(el) {
helper.removeClass(el, 'fade-in');
helper.addClass(el, 'fade-out');
}
function fadeIn(el) {
helper.removeClass(el, 'fade-out');
helper.addClass(el, 'fade-in');
}
And that brings us to an end of the native JavaScript code to load the tabs content from an AJAX source.
While there are a lot of script files involved here coming to a total of 7, it’s easily maintainable code that works on a wide range of web browsers. When it comes time to putting things in to production for a live web site, all of the script files can be easily compressed, for example by using the Online JavaScript Compression Tool
Download
Please feel free to download the vanilla tabs code and have an explore.
The ajax requests will work locally on all web browsers except for Google Chrome, for which you may want to instead use a local web server such as EasyPHP so that you can then place the code in its local web location, and get to it by loading up http://localhost/
And with Internet Explorer you will need to give permission for the local script to work, by clicking Allow Blocked Content.
Good luck, and have fun exploring!