JavaScript Challenge: Convert JQuery to Plain JavaScript

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!

Thanks Paul (and Pullo) for an interesting quiz and good explanations. There’s a lot to digest so I’ll have to work my way through it slowly :slight_smile:

In some ways, for amateurs in JS like me, I think you just proved the case for jquery rather than the opposite :). I was expecting the vanilla js version to be just a few lines longer but it was actually quite involved when you get into the detail. However, I appreciate that the JS version is obviously a lot shorter when you compare it to the jquery library itself.

This sounds like a good jumping off point to demonstrate a much smaller vanilla solution that works across all modern web browsers.
I’ll see what I can come up with by way of demonstration.

Here’s a fairly condensed version of the above code in just 23 lines. All it needs is the microAjax library.


Array.prototype.forEach.call(document.querySelectorAll('.tab a'), function (el) {
    el.onclick = function (evt) {
        var link = this, content = document.querySelector('#content');
        if (!link.classList.contains('active')) {
            content.classList.remove('fade-in');
            content.classList.add('fade-out');

            window.setTimeout(function () {
                Array.prototype.forEach.call(document.querySelectorAll(".tab a"), function (el) {
                    el.classList.remove('active');
                });
                link.classList.add('active');

                microAjax(link.href, function (response) {
                    content.innerHTML = response;
                });

                content.classList.remove('fade-out');
                content.classList.add('fade-in');
            }, 800);
        }
        return false;
    };
});
document.querySelector('.tab a').click();

It’s even shorter than the jQuery code too, but just because jQuery code can be done with modern vanilla JavaScript doesn’t I think mean that we should aim at writing code in the same manner as jQuery.

I think that we can write code that is better and more understandable than jQuery.

Thanks Paul, that certainly looks a lot neater and more succinct.

The classlist object certainly makes things easier ( although it seems was influenced by jquery a little). It would seem to be good if JS adopted some of these “shortcuts” that libraries produce to make life easier for the likes of me :slight_smile:

Nice work Paul W. and Pullo - excellent series of posts.

While I’d agree with Paul O’B that the non-jQuery version doesn’t look simpler/shorter, it is definitely worth noting that the bespoke code here will have a few benefits to it. It will perform better and be easy to maintain even by someone who doesn’t have any knowledge of jQuery. It would also be quite trivial to turn it in to it’s own little extendible library that can be included in other places.

Looking forward to seeing more of these quiz/competition things :slight_smile:

Hey Paul,

Could you take a second to explain (in high level terms) what is going on with this:

Array.prototype.forEach.call(document.querySelectorAll('.tab a'), function (el) { ... }

I get the general idea, but my understanding of call() is a little fuzzy.

Sure thing - the MDN for nodeList provides some useful code snippets for handling nodeLists, such as this one which iterates over them without extending the DOM


var forEach = Array.prototype.forEach;

var divs = document.getElementsByTagName( 'div' );
var firstDiv = divs[ 0 ];

forEach.call(firstDiv.childNodes, function( divChild ){
  divChild.parentNode.style.color = '#0F0';
});

What it does is to use the array forEach iterator to loop over each of the items in the nodeList. So long as that nodeList is not having changes made to it, such as nodes added or removed, that is a reliable solution.

The most reliable of course is to use a normal for loop to iterate over the nodeList, but using the array forEach can be a suitable alternative too.

While I did initially have the forEach variable declared in that condensed code that you quoted above, but to save another line of code, the Array.prototype.forEach was taken down in to the code where forEach.call is used, resulting in Array.prototype.forEach.call

Ah ok, the simplified example makes it easier to understand. Thanks.

So, expanding on what you said, say I had a bunch of div elements and I wanted to hide those with a class name of “invisible”.

What won’t work is this:

var divs = document.getElementsByTagName( 'div' );

divs.foreach(function(div){
  if (div.className === "invisible"){
    div.style.display = 'none';
  }
});

because divs is a NodeList, we can’t use Array methods on it.

So, to get this to work, we need to get a reference to the Array.prototype.foreach method:

var forEach = Array.prototype.forEach;

Then, we can use call on that method passing it a given this value and the callback function that it is expecting:

var forEach = Array.prototype.forEach,
    divs = document.getElementsByTagName( 'div' );

forEach.call(divs, function( div ){
  if (div.className === "invisible"){
    div.style.display = 'none';
  }
});

This can then be condensed to this (which is what I think confused me in the first place):

Array.prototype.forEach.call(document.getElementsByTagName('div'), function( div ){
  if (div.className === "invisible"){
    div.style.display = 'none';
  }
});

Did I get that right?

Yes indeed - although due to normally preferring understandability over compactness, a standard for loop is the preferred way to handling node lists.


var els = document.getElementsByTagName('div'),
    i;
for (i = 0; i < els.length; i += 1) {
  if (els[i].className === "invisible"){
    els[i].style.display = 'none';
  }
});

which if you prefer can be condensed down in to a dedicated function call:


function forEach(els, callback) {
  var i;
  for (i = 0; i < els.length; i += 1) {
    // standard Array.forEach parameters are el, index, array
    callback(els[i], i, els);
  }
}

forEach(document.getElementsByTagName('div'), function (div) {

Ultimately though in this particular situation, the best solution involves writing no JavaScript code at all :slight_smile:


div.invisible {
    display: none;
}