JavaScript Challenge: Convert JQuery to Plain JavaScript

In recent years, jQuery has become the de-facto JavaScript library on the web.
It irons out many cross-browser inconsistencies and adds a welcome layer of syntactic sugar to client-side scripting.
Yet, all too often we see jQuery being used to do something that vanilla JavaScript could do just as well, if not better.

That’s why we’ve decided to set you a little challenge: convert our jQuery into plain old JavaScript.

Successful entries will be those who get things working in any browser without a single jQueryism.
Those looking to score extra points can do so by making their JavaScript backwards compatible, so that it doesn’t break in older browsers.

The jQuery we want you to convert powers a simple tab-based navigation. The content of the tabs resides in different files which are loaded into the main page using AJAX.

You can see the code involved below, or download the files from here.
You can see the tab-based navigation in action here.
The code involved is listed below.

Entries need only include the JavaScript, but please use spoiler tags to hide the answers that you post.
The challenge ends June 16

For example, this:
[noparse]

[spoiler]Like this[/spoiler]

[/noparse]

Renders this:

[spoiler]Like this[/spoiler]

Please note: this won’t work locally in Chrome (owing to the local AJAX requests). To get around this, please either use a different browser, a local server stack (such as xampp or [URL=“http://www.easyphp.org/”]easyphp), or a remote server.

Good Luck!

index.html

<!DOCTYPE HTML>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Tabbed panels</title>
    <link rel="stylesheet" type="text/css" href="css/style.css">
</head>

<body>
    <h1>JS Tabs Challenge</h1>
    <p>All too often jQuery is used to do something that vanilla JavaScript can do just as well, if not better.<p>
    <p>Your challenge is to convert our jQuery into plain old JavaScript.</p>

    <div class="tabs">
        <ul class="nav">
            <li><a href="tab1.html">Tab 1</a></li>
            <li><a href="tab2.html">Tab 2</a></li>
            <li><a href="tab3.html">Tab 3</a></li>
        </ul>
    </div>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script type="text/javascript" src="js/tabs.js"></script>
</body>
</html>

tab1.html

<h2>Here is the heading for Tab#1</h2>

<p>Liquor ipsum dolor sit amet glenburgie pappy van winkle. Amaretto sour absolut clynelish macuá stinger singapore sling tanqueray vodka sundowner, ardmore wild turkey; colombia? Cactus jack alexander gin fizz kahlua royal bermuda cocktail the last word ballantine dalwhinnie aultmore smirnoff grand marnier! </p>

<p>Fish house punch brandy daisy brass monkey moonwalk. Murphy's corn n' oil dufftown agent orange kensington court special drumguish kamikaze; chopin french 75. Dufftown my fair lady. Jacquin imperial moscow mule. Van winkle family reserve sazerac; sundowner; sake manhattan jack rose kamikaze loch lomond. Remy martin lemon split mai-tai bowmore polmos krakow colorado bulldog.</p>

tab2.html

<h2>Here is the heading for Tab#2</h2>

<p>Negroni glenturret strega el presidente dalmore miltonduff, "tom and jerry." Singapore sling teacher's oban glogg jim beam whiskey sour edradour, glen elgin. Farnell seven and seven tormore, drambuie nikolaschka blue lagoon, ardmore chopin smirnoff charro negro sloe gin balmenach alexander. </p>

<p>Tom and jerry, piscola piña colada blair athol sea breeze kensington court special manhattan metaxa captain morgan. Speyside wolfram springbank greyhound; montgomery lime rickey matador churchill, churchill, ramos gin fizz harvey wallbanger deanston bloody aztec brackla. Polmos krakow kalimotxo, highland park metaxa hurricane paradise loch lomond early times?</p>

tab3.html

<h2>Here is the heading for Tab#3</h2>

<p>Polmos krakow madras dufftown cuba libre white horse, gimlet lejon v.o. tom collins. Ruby dutchess vodka sunrise, gin and tonic lagavulin buck's fizz: four score strathmill metaxas sake bomb tanqueray. Fettercairn salty dog kremlin colonel; spanish coffee, pegu white horse crown royal kahlua, "glen ord gin pahit; van winkle family reserve?" Mai-tai, salty dog. </p>

<p>Cosmopolitan glogg allt-á-bhainne j & b stinger balblair, fish house punch. Drambuie usher's sidecar long island iced tea paradise jameson kirsch creamsicle, brandy sour mudslide gordon's black russian tomintoul vodka sunrise." Jameson coronet vso fleischmann's bearded lady, metaxa, tomatin strathisla fettercairn gilbey's courvoisier.</p>

style.css

/* small reset as needed for this page only*/
html, body{
    margin: 0;
    padding: 0;  
}
h1, h2, p, ul { 
    margin: 0 0 15px;
    padding: 0;
}
ul {
    list-style: none;
}
h1, h2 {
    font-size: 140%;
}
/*  end reset */

body {
    padding: 25px;
}
.tabs {
    width: 600px;
    background: #f8f8f8;
    border: 1px solid #cccccc;
}
.nav {
    background: #E8E8E8;
    border-bottom: 1px solid #cccccc;
    float: left;
    width: 100%;
}
.nav li {float: left}
.nav a {
    float: left;
    padding: 8px 15px;
    text-decoration: none;
    font-weight: bold;
    border-right: 1px solid #cccccc;
}
.nav,.nav a,.nav a:visited{
    color: #484848;
    background: #E8E8E8;
}
.nav a:hover,
.nav a.selected { 
    background: #f8f8f8;
    position: relative;
    margin-bottom: -1px;
    padding-bottom: 9px;
}
.nav a.selected {
    cursor: default;
}
.content {
    padding: 10px 25px;
    clear: both;
    width: 550px; /* ie6/7 haslayout trip */
}

tabs.js

$(document).ready(function () {
    $(".tabs .nav a").on("click", function (e) {
        e.preventDefault();

        var $tab = $(this),
            fileToLoad = $tab.attr("href"),
            $nav = $tab.parents('.nav'),
            $content = $nav.siblings('.content');

        // Prevent the same tab from being reloaded
        if ($tab.hasClass("selected")) {
            return false;
        }

        // check if section for content should be added
        if (!$content.length) {
            $content = $('<div>').addClass('content');
            $content.insertAfter($tab.parents('.nav'));
        }

        // change selected tab and load new content
        $nav.find('a').removeClass("selected");
        $content.fadeOut('slow', function () {
            $tab.addClass("selected");
            $(this).load(fileToLoad).fadeIn();
        });
    });

    $('.tabs .nav a:first').trigger('click');
});

I’ve tried about 5 times now to post my code, but I keep getting a 500 error when I hit submit

Edit: OK, well it let me post this, so I’m guessing it’s got something to do with the length of the code maybe? I’ve put my attempt at a solution up on pastebin here: http://pastebin.com/PYyjGw0e

It doesn’t work on IE6 or 7 (but works on IE10) for some reason I can’t figure out - perhaps someone else can tell me? Also, just for the record, it’s not all my own code - I had to borrow snippets from here and there, but it’s a great challenge!

Damn it. I remember someone mentioning a bug like that in the past. I’ll have to get it looked at. Thanks for working around it.

This works in IE7,8,9 both standard and quirks, firefox, chrome, opera, and safari - hadn’t tested on anything else so not sure if it works or not on other browsers/versions

[spoiler]
		<script>
			DoAjax = function(id){
				
				if(id == undefined || id == '')
					return false;
				
				var a, page = 'page' + id + '.php';
				
				try{
					a = new XMLHttpRequest();
				} catch (e){
					try{
						a = new ActiveXObject("Msxml2.XMLHTTP");
					} catch (e) {
						try{
							a = new ActiveXObject("Microsoft.XMLHTTP");
						} catch (e){
							alert("Guess I lost lol");
							return false;
						}
					}
				}
				// Create a function that will receive data sent from the server
				a.onreadystatechange = function(){
					if(a.readyState == 4){
						document.getElementById('content').innerHTML = a.responseText;
					}
				}
				a.open("GET", "page" + id + ".php", true);
				a.send(null); 
			}                                            
			
			ClickListener = function(e,id) {
				if(id == null)
					var id = e.target.id;
				else
					var id = id;
				
				DoAjax(id);
			}
			
			var tab1 	= document.getElementById('1');
			var tab2 	= document.getElementById('2');
			var tab3 	= document.getElementById('3');
			
			try {
				tab1.addEventListener('click', ClickListener);
				tab2.addEventListener('click', ClickListener);
				tab3.addEventListener('click', ClickListener);
			} catch(e) {	}
			
			//If you want to init on 1st page 
			//DoAjax(1);
			
		</script>
[/spoiler]

Here’s my shot in coffeescript, I’ve only included the basics for modern browsers and not implemented things like fading which I would implement in CSS with transitions.

[spoiler]
content = null

load = (el, url, callback)->
  xhr = new XMLHttpRequest()
  xhr.onreadystatechange = ->
    el.innerHTML = @.responseText if @readyState == 4
    callback()
  xhr.open "GET", url, true
  xhr.send null

click = (event)->
  el = event.target
  if el.href
    event.preventDefault()
    li = event.target.parentNode()
    load content, event.target.href, ->
      li.className = 'fade-in'
      setTimeout ->
        li.className = ''
      , 1000

document.onreadystatechange = ->
  if document.readyState == "interactive"
    content = document.querySelector '.content'
    document.addEventListener 'click', click
[/spoiler]

converted to js.


[spoiler]
(function() {

var click, content, load;

content = null;

load = function(el, url, callback) {
  var xhr;
  xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState === 4) {
      el.innerHTML = this.responseText;
    }
    return callback();
  };
  xhr.open("GET", url, true);
  return xhr.send(null);
};

click = function(event) {
  var el, li;
  el = event.target;
  if (el.href) {
    event.preventDefault();
    li = event.target.parentNode();
    return load(content, event.target.href, function() {
      li.className = 'fade-in';
      return setTimeout(function() {
        return li.className = '';
      }, 1000);
    });
  }
};

document.onreadystatechange = function() {
  if (document.readyState === "interactive") {
    content = document.querySelector('.content');
    return document.addEventListener('click', click);
  }
};

})();
[/spoiler]

OK, here’s the revised version of my code - compatible with IE6+ now, and implements all the features of the original script.

[spoiler]var TimeToFade = 1000.0;

function animateFade(lastTick, eid, callback)
{
  var curTick = new Date().getTime();
  var elapsedTicks = curTick - lastTick;

  var element = document.getElementById(eid);

  if(element.FadeTimeLeft <= elapsedTicks)
  {
    element.style.opacity = element.FadeState == 1 ? '1' : '0';
    element.style.filter = 'alpha(opacity = ' + (element.FadeState == 1 ? '100' : '0') + ')';
    element.FadeState = element.FadeState == 1 ? 2 : -2;
    if (typeof callback == 'function') {
        callback();
    }
    return;
  }

  element.FadeTimeLeft -= elapsedTicks;
  var newOpVal = element.FadeTimeLeft/TimeToFade;
  if(element.FadeState == 1)
    newOpVal = 1 - newOpVal;

  element.style.opacity = newOpVal;
  element.style.filter = 'alpha(opacity = ' + (newOpVal*100) + ')';

  //setTimeout("animateFade(" + curTick + ",'" + eid + "')", 33);
  setTimeout(function(){animateFade(curTick, eid, callback)}, 33);
}

function fade(eid, callback)
{
  var element = document.getElementById(eid);
  if(element == null)
    return;


  if(element.FadeState == null)
  {
    if(element.style.opacity == null || element.style.opacity == ''
       || element.style.opacity == '1')
      element.FadeState = 2;
    else
      element.FadeState = -2;
  }

  if(element.FadeState == 1 || element.FadeState == -1)
  {
    element.FadeState = element.FadeState == 1 ? -1 : 1;
    element.FadeTimeLeft = TimeToFade - element.FadeTimeLeft;
  }
  else
  {
    element.FadeState = element.FadeState == 2 ? -1 : 1;
    element.FadeTimeLeft = TimeToFade;
    //setTimeout("animateFade(" + new Date().getTime() + ",'" + eid + "')", 33);
    setTimeout(function(){animateFade(new Date().getTime(), eid, callback)}, 33);
  }
}

(function(window, document, undefined){

    if(typeof window.XMLHttpRequest === 'undefined' &&
        typeof window.ActiveXObject === 'function') {
        window.XMLHttpRequest = function() {
            try { return new ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch(e) {}
            try { return new ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch(e) {}
            return new ActiveXObject('Microsoft.XMLHTTP');
        };
    }

    var addEvent = function(element, evType, fn, useCapture) {
        if (element.addEventListener) {
            element.addEventListener (evType, fn, useCapture);
            return true;
        } else if (element.attachEvent) {
            var r = element.attachEvent('on' + evType, fn);
            return r;
        } else {
            element['on' + evType] = fn;
        }
    };

    var addClass = function(element, classname) {
        var cn = element.className;
        //test for existance
        if( cn.indexOf( classname ) != -1 ) {
            return;
        }
        //add a space if the element already has class
        if( cn != '' ) {
            classname = ' '+classname;
        }
        element.className = cn+classname;
    }

    var removeClass = function(element, classname) {
        var cn = element.className;
        var rxp = new RegExp( "\\\\s?\\\\b"+classname+"\\\\b", "g" );
        cn = cn.replace( rxp, '' );
        element.className = cn;
    }

    var hasClass = function(element, className) {
        var cn = element.className;
        //test for existance
        if( cn.indexOf( className ) != -1 ) {
            return true;
        }
        return false;
    }

    var getParent = function(element, className) {
        if (!element) {
            return element;
        } else if (hasClass(element, className)) {
            return element;
        } else {
            return getParent(element.parentNode, className);
        }
    };

    var getSibling = function(element, className) {
        var siblings = element.parentNode.children,
            j = siblings.length;

        for (var i=0; i < j; i++) {
            if (hasClass(siblings[i], className)) {
                var sibling = siblings[i];
            }
        }

        return sibling;
    };

    var loadTabContent = function(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4) {
                callback(xhr.responseText);
            }
        }
        xhr.open('GET', url, true);
        xhr.send(null);
    };

    var loadTab = function(event) {
        event.preventDefault ? event.preventDefault() : event.returnValue = false;

        var $tab = event.target || event.srcElement,
            fileToLoad = $tab.getAttribute("href"),
            $nav = getParent($tab, 'nav'),
            $content = getSibling($nav, 'content');

        if (hasClass($tab, 'selected')) {
            return false;
        }

        //check if section for content should be added
        if (typeof $content == 'undefined') {
            $content = document.createElement('div');
            $content.id = 'content';
            addClass($content, 'content');
            $nav.parentNode.appendChild($content);
        }

        //change selected tab and load new content
        for (var i=0; i < links.length; i++) {
            removeClass(links[i], 'selected');
        }

        fade('content', function(){
            loadTabContent(fileToLoad, function(responseText){
                addClass($tab, 'selected');
                $content.innerHTML = responseText;
                fade('content');
            })
        });


    };

    var lists = document.getElementsByTagName('li');
    var links = [];

    for (i=0; i < lists.length; i++) {
        var link = lists[i].getElementsByTagName('a')[0];
        links.push(link);
        addEvent(link, "click", loadTab, false);
    }

})(window, document);[/spoiler]

Great entries, guys!
Keep them coming!

Some helpers for those who may be stuck.

$(document).ready(function () {

$(“.tabs .nav a”)

.on(“click”, function (e) {
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener

$tab.attr(“href”)
https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute

$tab.parents(‘.nav’)
https://developer.mozilla.org/en-US/docs/Web/API/Node.parentNode
Needs some kind of looping to replicate .parents()

$nav.siblings(‘.content’)
https://developer.mozilla.org/en-US/docs/Web/API/Node.childNodes
Use with parentNode to get siblings.

$tab.hasClass(“selected”)
https://developer.mozilla.org/en-US/docs/Web/API/document.getElementsByClassName

$(‘<div>’)
https://developer.mozilla.org/en-US/docs/Web/API/document.createElement

.addClass(‘content’)
https://developer.mozilla.org/en-US/docs/Web/API/element.className
https://developer.mozilla.org/en-US/docs/Web/API/element.classList
see classList.add and classList.remove

$nav.find(‘a’)
https://developer.mozilla.org/en-US/docs/Web/API/element.getElementsByTagName

.load(fileToLoad)
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

.fadeIn()
http://bavotasan.com/2011/a-simple-fade-with-css3/
Use CSS and add/remove a class to the element with javascript className

.trigger(‘click’)
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent

Always check the browser compatibility at the bottom of these pages. Some of these will not work in IE8 and below, you will need to check the non-standard method which matches - there’s almost always some alternative (eg addEventListener > attachEvent).

There may be better alternatives to those which I have suggested - if you know of a better alternative please post a link and some comment about why it’s better.

In my opinion, the main complexity here is dealing with the ajax - ie the .load function in jquery.

I admit I do write a fair bit of native javascript for libraries where I can’t assume jquery is available. But if I’m working on a web app, I almost always use jquery because a) it’s well known so contributors can jump right in and b) it saves a lot of time and typing because it’s generally less verbose than javascript.

Some good tips there they may even tempt me to have a go with my limited JS knowledge :slight_smile:

Although my solution may not be coded as elegantly as many of my fellow contestants, I couldn’t resist having a go at this one.

I wish I had seen Ians post before I started… I am definitely bookmarking those links for later refence!

~90 lines of code with comments/and .js fade ( hto, yeah, I might have used CSS3 for that, I felt the spirit of the contest was to emulate the as much jQuery functionality as i technically could). Also I opted ‘functionalize’ many things, the idea being that in the real world code that can be reused is more valuable, that just a dedicated script for say making tabs. Though I haven’t done extensive testing yet (other than the current big 5) , intended my code to cater to legacy browsers that do not support things like querySelector.

[SPOILER]

function getElementsByClass(theClass,root,tag){
	theClass=' '+theClass+' ';
	var i, c, tl,checkClass;
 	var tag = ( tag === undefined) ? "*" : tag;
	var root = (typeof root === 'string') ? document.getElementById(root) : root;
	var root = (root === undefined) ? document : root;
	var allTags=root.getElementsByTagName(tag);
	var selectedClass = new Array();
	for (i=0, c=0, tl=allTags.length; i<tl; i++) {
		checkClass=' '+allTags[i].className+' ';
		if (checkClass.indexOf(theClass)>-1) {
			selectedClass[c]=allTags[i];
			c++;
		}
	}
	return selectedClass;
}
function AJAX(url){ // overly simplyfied AJAX foo for this challenge only!!!
  	if (window.XMLHttpRequest){ var req=new XMLHttpRequest();}
 	else if(window.ActiveXObject){ var req= new ActiveXObject("Microsoft.XMLHTTP")}
 	else {return false }
  	req.open('GET', url, true);
  	req.send(''); 
 	return req;		   
 }
function removeClass(strn,clssToKill){
			strn=' '+strn+' ';
			strn=strn.replace(' '+clssToKill+' ','');
			return trim(strn);
}
function trim(strn){return strn.replace(/^\\s\\s*/, '').replace(/\\s\\s*$/, '');} // simplyfied Trim foo for this challenge only!!!
function fadeFoo(el,curr,int){ //simplified fade foo
			var IE= (typeof el.contLnk.style.opacity ==='string');
			curr=(curr<0) ?  0 :curr; 
			if (IE) {el.contLnk.style.opacity=curr+'';}
			else{ el.style.filter = 'alpha(opacity=' + curr*100 + ')';}
			if (curr == 0 && int<0){
				el.contLnk.innerHTML=el.sw;
				el.sw='';
				int=-1*int;
			}
			curr=curr+int;
			if (curr < 1){var to=setTimeout(function(){fadeFoo(el,curr,int)},50);}
			if (curr>=1) {
				if (IE) {el.contLnk.style.opacity='1';}
				else{el.style.filter = 'alpha(opacity=100)';}
			}
 }
function clickFoo(el){
	if ((' '+el.className+' ').indexOf('selected') > -1) { return;} 
 	var iii,req, tempClss,parIts, parL, par=el.parentNode.parentNode.getElementsByTagName('a');
	for (iii=0,parL=par.length ; iii<parL; iii++){
			par[iii].className=removeClass(par[iii].className,'selected');
	}
	req=AJAX(el.href);
	req.onreadystatechange = function(){
		if(req.readyState == 4  ){
	   		el.className=el.className+" selected";
	   		el.t=setTimeout(function(){fadeFoo(el,1,-.05)},0);
	   		el.sw=req.responseText;
	   	}
	}
}
  	// preps dipslpay container 
cont=document.createElement('div');
cont.class='content'; 
temp=Array();
temp1=Array();
	 // seaches for designated class ( tag.class also possible)
     tabS=getElementsByClass('tabs',undefined,'div'); 
   	  for (i=0, l=tabS.length; i<l; i++) {	
  	  temp=getElementsByClass('nav',tabS[i],'ul');
	  // make display area
 	 daClone=cont.cloneNode(true);
 	 tabS[i].appendChild(daClone);
      //  attach event to links
 		for (j=0, sln=temp.length; j<sln; j++) {
			temp1=temp[j].getElementsByTagName('a');
  			for (k=0, slk=temp1.length; k<slk; k++) {
 				temp1[k].contLnk=daClone;
		 		temp1[k].onclick=function(e){
		 				e.preventDefault();
		 				var self=this;
	 	 				clickFoo(self);
	 	 		}
  			}
  			clickFoo(temp1[0]); 
  		  }
 	}
[/SPOILER]

This should work in most browsers, although I did not do thorough testing as I don’t have every browser installed on this machine.

I know this code is lengthy, but I would rather have it legible for future revisions that compact and impossible to understand a year from now when I have to revisit it.

I hope this helps someone. :slight_smile:

There are two versions of many of these functions, one for modern standards-compliant browsers, and one for Internet Explorer versions 8 and earlier. If I were to use this code in my own page I would likely place all of the legacy code in a separate file and wrap it in an IE conditional comment like this:


<![if lt IE 9]>
<script type="text/javascript" src="tabsQuirks.js"></script>
<![endif]>

tabs.js

[spoiler]
// Copyright 2013, Kevin Douglas, All rights reserved
// please learn from this but don't just copy it
// Kevin Douglas: kdouglas@satarah.com
//
// Notes:
// I have simplified error checking and fault tolerance for this challenge
// I have used procedural vs object-oriented techniques for this challenge
// both in an effort to minimize code and complexity


// *** TAILORED FUNCTIONALITY TO THIS ENVIRONMENT ***

nodeEvent(window, 'load', main, 'attach');

function main()
{
	// get node references
	var tabs = nodeClass(document, 'tabs', 'find')[0];
	var nav = nodeClass(tabs, 'nav', 'find')[0];
	var links = nav.getElementsByTagName('a');
	
	// create-append content node
	var div = tabs.appendChild(document.createElement('div'));
	nodeClass(div, 'content', 'attach');
	
	// set handlers on link nodes
	for (var i = 0; i < links.length; i++)
	{
		nodeEvent(links.item(i), 'click', linkEvents, 'attach');
	}
	
	// load default tab and content
	nodeEventTrigger(links.item(0), 'click');
}

function linkEvents(event)
{
	// cancel default click event
	nodeEventCancel(event);
	
	// get specific tab node
	var link = (typeof event.target == 'object') ?
		event.target : // IE 9+, Moz, etc.
		event.srcElement; // IE 6 - 8
	if (link.nodeType == 3) // Safari
	{
		link = link.parentNode;
	}
		
	// if link is currently selected tab, exit
	if (nodeClass(link, 'selected', 'test'))
	{
		return;
	}
	
	// get node references
	var div = nodeClass(document, 'content', 'find')[0];
	var prev = nodeClass(document, 'selected', 'find')[0];
	
	// load new tab content via AJAX
	getFile(link.href, loadContent);
	
	// remove class from prev tab
	if (typeof prev == 'object') // else error on page load
	{
		nodeClass(prev, 'selected', 'remove');
	}

	// attach class to new tab
	nodeClass(link, 'selected', 'attach');
	
	// hide prev content
	nodeOpacity(div, 'hide', false);
}

function loadContent(data)
{
	// get node reference
	var div = nodeClass(document, 'content', 'find')[0];
	
	// wait for fade animation to complete
	var handler = window.setInterval(function()
	{
		if (typeof nodeOpacity.handler == 'undefined' &&
		typeof nodeOpacityQuirks.handler == 'undefined')
		{
			div.innerHTML = data;
			window.clearInterval(handler);
			nodeOpacity(div, 'show', false);
		}
	}, 250);
}


// *** REUSABLE FUNCTIONALITY IN OTHER ENVIRONMENTS ***

// this is a trivial GET AJAX approach
function getFile(target, callback)
{
	function handleResponse()
	{
		if (xhr.readyState == 4) // response recieved
		{
			if (xhr.status == 200) // 'OK'
			{
				callback(xhr.responseText)
			}
			else // response error
			{
				throw new Error(xhr.status);
			}
		}
	}
	var xhr = (typeof window.XMLHttpRequest == 'function') ?
		new XMLHttpRequest() : // IE 9+, Moz, etc.
		new ActiveXObject("Microsoft.XMLHTTP"); // IE 6 - 8
	xhr.onreadystatechange = handleResponse;
	xhr.open('GET', target, true);
	xhr.send(null);
}

// action = 'remove'|'attach'|'find'|'test', node = document for 'find' action, or a specific node
function nodeClass(node, value, action)
{
	var classes = node.className;
	var pattern = new RegExp('(^| )' + value + '( |$)', 'i'); // case-insensitive
	switch(action)
	{
		case 'remove':
		{
			classes = classes.replace(pattern, "$1"); // remove pattern and any leading whitespace
			classes = classes.replace(/ $/, ''); // remove any end-of-string whitespace
			node.className = classes;
			return node.className;
		}
		case 'attach':
		{
			node.className = (classes.length) ? // existing classes?
				classes + ' ' + value : value; // prefix whitespace or not
			return node.className;
		}
		case 'find':
		{
			var found = [];
			var nodes = node.getElementsByTagName("*");
			for (var i = 0; i < nodes.length; i++)
			{
				if (pattern.test(nodes[i].className))
				{
					found[found.length] = nodes[i];
				}
			}
			return found;
		}
		case 'test':
		{
			// fallthrough
		}
		default:
		{
			return pattern.test(classes);
		}
	}
}

// IE 9+, Moz, etc.
// action = 'attach'|'remove'
function nodeEvent(node, type, handler, action)
{
	if (typeof node.addEventListener == 'undefined')
	{
		nodeEventQuirks(node, type, handler, action);
		return;
	}
	switch(action)
	{
		case 'attach':
		{
			node.addEventListener(type, handler, false);
			break;
		}
		case 'remove':
		{
			node.removeEventListener(type, handler, false);
			break;
		}
	}
}

// IE 6 - 8 *** develop for standards, devolve for legacy support ***
function nodeEventQuirks(node, type, handler, action)
{
	switch(action)
	{
		case 'attach':
		{
			node.attachEvent('on' + type, handler);
			break;
		}
		case 'remove':
		{
			node.detachEvent ('on' + type, handler);
			break;
		}
	}
}

// IE 9+, Moz, etc.
function nodeEventCancel(event)
{
	if (typeof event.preventDefault == 'undefined')
	{
		nodeEventCancelQuirks(event);
		return;
	}
	event.preventDefault();
}

// IE 6 - 8 *** develop for standards, devolve for legacy support ***
function nodeEventCancelQuirks(event)
{
	if (typeof event == 'undefined')
	{
		event = window.event;
	}
	event.returnValue = false;
	return false;
}

// IE 9+, Moz, etc.
function nodeEventTrigger(node, type)
{
	if (typeof document.createEvent == 'undefined')
	{
		nodeEventTriggerQuirks(node, type);
		return;
	}
	var event = document.createEvent('HTMLEvents');
	event.initEvent(type, true, true); // event type, bubbling, cancelable
	node.dispatchEvent(event);
}

// IE 6 - 8 *** develop for standards, devolve for legacy support ***
function nodeEventTriggerQuirks(node, type)
{
	var event = document.createEventObject();
	node.fireEvent('on' + type, event)
}

// IE 9+, Moz, etc.
// action = 'show'|'hide', revert = bool
nodeOpacity.handler;
function nodeOpacity(node, action, revert)
{
	if (typeof node.style.opacity == 'undefined')
	{
		nodeOpacityQuirks(node, action, revert);
		return;
	}
	if (typeof nodeOpacity.handler == 'number')
	{
		window.clearInterval(nodeOpacity.handler)
		nodeOpacity.handler = undefined;
	}
	var fps = 12; // frames or steps per second
	var increment = 1.25; // change per step, Note: 10 is evenly divisible by this value (8 steps)
	var interval = Math.round(1000 / fps); // timer microseconds
	var disolve = (action.toLowerCase() == 'hide') ? true : false; // 'hide'|'show'
	var opacity = (disolve) ? 10 : 0; // start value
	var outcome = (disolve) ? 0 : 10; // end value
	nodeOpacity.handler = window.setInterval(function ()
	{
		opacity = opacity + (disolve ? - increment : increment);
		node.style.opacity = opacity / 10; // set node opacity
		//console.log(node.style.opacity); // debugging
		if (opacity == outcome) // NOT decimal tolerant
		{
			window.clearInterval(nodeOpacity.handler); // exit transformation
			nodeOpacity.handler = undefined; // reset handler
			if (revert) // fade out-in | in-out behavior
			{
				nodeOpacity(node, (disolve ? 'show' : 'hide'), false);
			}
		}
	}, interval);
}

// IE 6 - 8, *** develop for standards, devolve for legacy support ***
// action = 'show'|'hide', revert = bool
nodeOpacityQuirks.handler;
function nodeOpacityQuirks(node, action, revert)
{
	if (typeof nodeOpacityQuirks.handler == 'number')
	{
		window.clearInterval(nodeOpacityQuirks.handler);
		nodeOpacityQuirks.handler = undefined;
	}
	var fps = 12; // frames or steps per second
	var increment = 12.5; // change per step, Note: 100 is evenly divisible by this value (8 steps)
	var interval = Math.round(1000 / fps); // timer microseconds
	var disolve = (action.toLowerCase() == 'hide') ? true : false; // 'hide'|'show'
	var opacity = (disolve) ? 100 : 0; // start value
	var outcome = (disolve) ? 0 : 100; // end value
	nodeOpacityQuirks.handler = window.setInterval(function ()
	{
		opacity = opacity + (disolve ? - increment : increment);
		node.style.filter = 'alpha(Opacity=' + opacity + ')'; // set node opacity
		//console.log(opacity); // debugging
		if (opacity == outcome) // NOT decimal tolerant
		{
			window.clearInterval(nodeOpacityQuirks.handler); // exit transformation
			nodeOpacityQuirks.handler = undefined;
			if (revert) // fade out-in | in-out behavior
			{
				nodeOpacityQuirks(node, (disolve ? 'show' : 'hide'), false);
			}
		}
	}, interval);
}

[/spoiler]

This was an interesting little challenge. I’m sure I’m known on the forums to jump to jQuery solutions quite quickly. One of my main reasons for this is because then you don’t have to deal with browser quirks.

Writing the code for this challenge has once again reminded me of how crappily the DOM has been implemented in various browsers :wink:

I wanted to emulate a lot of the jQuery functionality, yet still keep the code relatively lean. So obviously I haven’t implemented a whole Animation or DOM framework, but some rudimentary methods are there. And to be honest if I’d build this for a real project, I would abstract all the relative functionalities out in to appropriate classes.

One of the important things I also wanted to achieve is a solution that could cater to multiple sets of tabs on a page - should that ever be required.

The only limitation of this solution is that you can only pass in a singular selector to the constructor (i.e. only “#some-id” or “.some-class” but not “.some-class .tabs” or “.tabs, .some-other-tabs”). This limitation only really exists for IE7 and below though as the solution for modern browsers implements querySelectorAll.

So, coming in at around 575 lines, here is my solution:

Also on pastebin for those wanting to see pretty code: http://pastebin.com/GS8z7LEH
Also also on http://afterlight.com.au/sitepoint/js-challenge/ if you want to see the solution in action

[spoiler];(function(window, document){
    "use strict";


    /**
     * Async abstractions
     *
     * This class adapted from http://www.quirksmode.org/js/xmlhttp.html
     *
     * @returns {AsyncRequestObject}
     * @constructor
     */
    function AsyncRequestObject() {


        var self = this;


        var XMLHttpFactories = [
            function () {return new XMLHttpRequest();},
            function () {return new ActiveXObject("Msxml2.XMLHTTP");},
            function () {return new ActiveXObject("Msxml3.XMLHTTP");},
            function () {return new ActiveXObject("Microsoft.XMLHTTP");}
        ];


        /**
         * Create an XMLHttpObject based on which of the factories
         * returns an object that can be used.
         *
         * @returns {XMLHttpRequest}
         */
        function createXMLHTTPObject() {
            var xmlhttp = false;
            for (var i=0;i<XMLHttpFactories.length;i++) {
                try {
                    xmlhttp = XMLHttpFactories[i]();
                }
                catch (e) {
                    continue;
                }
                break;
            }
            return xmlhttp;
        }


        /**
         * Perform a request to a given URL
         *
         * Options can include:
         *  postData  - an object containing data to be posted
         *  error     - an error callback
         *  success   - a success callback
         *
         * @param {String} url
         * @param {Object} options
         */
        this.request = function request(url, options) {
            var method;


            if (!this.xhr) {
                return;
            }


            method = (options.postData) ? "POST" : "GET";


            this.xhr.open(method,url,true);


            if (options.postData) {
                this.xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
            }


            this.xhr.onreadystatechange = function () {
                if (self.xhr.readyState !== 4) {
                    return;
                }


                if (self.xhr.status !== 200 && self.xhr.status !== 304) {
                    if (options.error) {
                        options.error(self.xhr);
                    }
                    return;
                }
                options.success(self.xhr);
            };


            if (self.xhr.readyState === 4) {
                return;
            }


            self.xhr.send(options.postData);
        };




        this.xhr = createXMLHTTPObject();
    }


    window.AsyncRequestObject = AsyncRequestObject;




    /**
     * Tabs class - will facilitate a simple HTML pattern
     * to be turned in to AJAX tabs
     *
     * @param {String} selector
     * @returns {Tabs}
     * @constructor
     */
    function Tabs(selector) {


        var self = this;


        this.selector = selector;


        //
        // N.B.: We use this method signature to aid with debugging.
        //
        // this.methodName = function methodName() {}
        //
        // So instead of seeing calls to "anonymous function" in the stack
        // we will actually see the function name.
        //




        /**
         * Fade an element in from 0 opacity to 100% opacity over a
         * given duration and when finished fire an optional callback.
         *
         * @param {Node}     el
         * @param {Number}   duration
         * @param {Function} [callback]
         */
        this.fadeIn = function fadeIn(el, duration, callback) {
            this.fadeTo(el, 0, 100, duration, callback);
        };




        /**
         * Fade an element out from 100 to 0% opacity over a
         * given duration and when finished fire an optional callback.
         *
         * @param {Node}     el
         * @param {Number}   duration
         * @param {Function} [callback]
         */
        this.fadeOut = function fadeOut(el, duration, callback) {
            this.fadeTo(el, 100, 0, duration, callback);
        };




        /**
         * Fade an element from one opacity to another over a
         * given duration and when finished fire an optional callback
         *
         * @param {Node}     el
         * @param {Number}   from
         * @param {Number}   to
         * @param {Number}   duration
         * @param {Function} [callback]
         */
        this.fadeTo = function fadeTo(el, from, to, duration, callback) {
            var startOpacity = parseInt(from, 10),
                finalOpacity = parseInt(to, 10),
                difference = Math.abs(finalOpacity - startOpacity),
                tickDuration = parseInt(duration, 10) / difference,
                currentOpacity = startOpacity;


            // Increase or Decrease the currentOpacity based on
            // which direction we need to go.
            function setCurrentOpacity() {
                currentOpacity = startOpacity < finalOpacity ? ++currentOpacity : --currentOpacity;
            }


            // Check if the currentOpacity is out side of
            // the final opacity we want to achieve.
            function currentOpacityOutOfBounds() {
                return startOpacity < finalOpacity ? (currentOpacity > finalOpacity) : (currentOpacity < finalOpacity);
            }


            // Recurse through this function until we've met
            // the stop condition.
            function fade() {


                // our stop condition is that the current
                // opacity is out of bounds
                if (currentOpacityOutOfBounds()) {


                    //if we have a callback - call it now.
                    if (typeof callback === "function") {
                        callback.call(el);
                    }
                    return;
                }


                // set all the styles
                el.style.MsFilter = "progid:DXImageTransform.Microsoft.Alpha(Opacity="+ currentOpacity +")";
                el.style.filter = "alpha(opacity="+ currentOpacity +")";
                el.style.opacity = currentOpacity / 100;


                setCurrentOpacity();


                setTimeout(fade, tickDuration);
            }


            fade();
        };




        /**
         * Load a file and pass its contents along to the callback
         *
         * @param path
         * @param callback
         */
        this.load = function load(path, callback) {


            this.async.request(path, {
                success: function( xhr ){
                    callback.call(self, xhr.responseText);
                },
                error: function() {
                    callback.call(self, "Sorry, could not retrieve the content for this tab");
                }
            });


        };




        /**
         * Check if an element has a class
         *
         * @param {Node}   el
         * @param {String} className
         */
        this.hasClass = function hasClass(el, className) {
            var classArray, classListLength, i;


            if (!el) {
                return;
            }


            classArray = el.className.split(" ");
            classListLength = classArray.length;


            // not the most efficient way of searching, but it works :)
            for ( i = 0; i < classListLength; i++ ) {
                if (classArray[i] === className) {
                    return true;
                }
            }


            return false;
        };




        /**
         * Add a class to an element.
         *
         * @param {Node}   el
         * @param {String} newClass
         */
        this.addClass = function addClass(el, newClass) {


            if (!el) {
                return;
            }


            if ( ! this.hasClass(el, newClass)) {
                // only append the class name if it's not already present.
                el.className += newClass;
            }


            return el;
        };




        /**
         * Remove a class from an element
         *
         * @param {Node} el
         * @param {String} oldClass
         */
        this.removeClass = function removeClass(el, oldClass) {
            var classArray, classArrayLength, i, newClassList = [];


            if (!el) {
                return;
            }


            if (this.hasClass(el, oldClass)) {
                //only try to remove the class if its present


                classArray = el.className.split(" ");
                classArrayLength = classArray.length;


                // not a terribly efficient way to do it - but it's
                // clear what the code does
                for( i = 0; i < classArrayLength; i++ ) {
                    if ( classArray[i] !== oldClass ) {
                        // we add the classes to an array to keep DOM manipulation to a minimum
                        newClassList.push(classArray[i]);
                    }
                }


                el.className = newClassList.join(" ");
            }


            return el;
        };




        /**
         * Add an event listener to a DOM node
         *
         * @param {Node}     el
         * @param {String}   eventName
         * @param {Function} callback
         */
        this.addEventListener = function addEventListener(el, eventName, callback) {


            if (window.addEventListener) {
                el.addEventListener(eventName, function(e) {
                    callback.call(this, e);
                });
                return;
            }


            if (window.attachEvent) {
                el.attachEvent("on"+eventName, function(e) {
                    e = e || window.event;
                    e.preventDefault  = e.preventDefault  || function(){e.returnValue = false;};
                    e.stopPropagation = e.stopPropagation || function(){e.cancelBubble = true;};
                    callback.call(el, e);
                });
            }


        };




        /**
         * Dispatch an event to a node
         *
         * @param {Node}   el
         * @param {String} eventName
         */
        this.dispatchEvent = function dispatchEvent(el, eventName) {
            var e;


            if (document.createEvent) {
                e = document.createEvent("HTMLEvents");
                e.initEvent(eventName, true, true);
                el.dispatchEvent(e);
                return;
            }


            if (document.createEventObject) {
                e = document.createEventObject();
                el.fireEvent("on" + eventName, e);
                return;
            }


        };




        /**
         * Create a new DOM el and append it to a container Node
         * and optionally give it a class. Return the newly added element.
         *
         * @param {Node}   container
         * @param {String} nodeName
         * @param {String} [nodeClassName]
         * @returns {HTMLElement}
         */
        this.appendNode = function appendNode(container, nodeName, nodeClassName) {
            var frag, newNode;


            // add a placeholder for the content
            frag = document.createDocumentFragment();
            newNode = document.createElement(nodeName);
            newNode.className = nodeClassName || "";
            frag.appendChild(newNode);


            container.appendChild(frag);
            return newNode;
        };




        /**
         * Return an array of elements based on a selector.
         *
         * @param selector
         * @returns {*} A NodeList or an Array
         */
        this.getElementsBySelector = function getElementsBySelector(selector) {


            // Modern browsers are cool - not much work to be done.
            if (document.querySelectorAll) {
                return document.querySelectorAll(selector);
            }


            //check if the selector starts with a "#" so we can use getElementById
            if (selector.indexOf("#") === 0) {
                return [document.getElementById(selector.replace("#", ""))];
            }


            // Check if the selector starts with a "." so we can use getElementsByClassName
            // We also know that we'll have document.getElementsByClassName as we've used a polyfill.
            if (selector.indexOf(".") === 0) {
                return document.getElementsByClassName(selector.replace(".", ""));
            }


            return false;


        };




        /**
         * Callback to fire when the content for a tab has loaded.
         *
         * @param {String} content The HTML to be injected
         */
        this.tabContentLoaded = function tabContentLoaded(content) {


            self.fadeOut(self.tabContentEl, 300, function() {
                // inject the content in to a new element.
                self.tabContentEl.innerHTML = content;
                self.fadeIn(self.tabContentEl, 800);
            });


        };




        /**
         * Handle the Click event on tabs
         * @param e
         */
        this.tabClickHandler = function tabClickHandler(e) {
            var target = e.srcElement || e.target;


            e.preventDefault();
            if ( target.nodeName === "A" ) {
                e.stopPropagation();


                // prevent reloading the same tab
                if (self.hasClass(target, "selected")) {
                    return false;
                }


                self.removeClass(self.currentTabEl, "selected");
                self.currentTabEl = target;
                self.addClass(self.currentTabEl, "selected");
                self.load(target.href, self.tabContentLoaded);


                return true;
            }


        };




        /**
         * Bind UI related events here
         */
        this.bindEvents = function bindEvents() {


            this.addEventListener(this.el, "click", this.tabClickHandler);


        };




        /**
         * Initialise the class
         *
         * Here we'll set up some default values and references to DOM elements.
         *
         */
        this.init = function init() {
            var i;


            this.tabs = [];


            // We do some checking to see if we  need to instantiate ourselves and pass
            // along an element or if we can simply proceed if a single element is found.
            // Ideally this would be abstracted to a DOM helper class.
            if (typeof this.selector === "string") {
                this.el = this.getElementsBySelector(this.selector);
            }
            else {
                this.el = this.selector;
            }


            if (this.el && this.el.length > 1) {
                // We have multiple elements - so we'll create new
                // Tab instances for each set of tabs
                for ( i = 0; i < this.el.length; i++) {
                    this.tabs.push(new Tabs(this.el[i]));
                }


                return;
            }
            else if (this.el && this.el.length === 1) {
                // just a single element we can prodeed with that.
                this.el = this.el[0];
            }
            else {
                // we end up here when the selector is not a string,
                // in which case it would be an element
                this.el = this.selector;
            }




            // get a reference to the nav container
            this.tabsNavContainer = this.el.getElementsByTagName("ul")[0]; // we definitely only want the first element


            // find all the tabs
            this.tabLinks = this.tabsNavContainer.getElementsByTagName("a");


            //inject a container for the content
            this.tabContentEl = this.appendNode(this.el, "div", "content");


            // AJAX functionality is abstracted
            this.async = new AsyncRequestObject();


            // Bind all the UI events
            this.bindEvents();


            // trigger a click on the first tab.
            this.dispatchEvent(this.tabLinks[0], "click");
        };




        //////////////////////////////////////////////////////////////////////////
        //
        // Borrowed from https://gist.github.com/eikes/2299607
        //
        // Note that we only really need this for < IE8 as IE8 and
        // up supports document.querySelectorAll
        //
        //////////////////////////////////////////////////////////////////////////
        //
        // Add a getElementsByClassName function if the browser doesn't have one
        // Limitation: only works with one class name
        // Copyright: Eike Send http://eike.se/nd
        // License: MIT License
        //


        if ( !document.getElementsByClassName ) {
            document.getElementsByClassName = function (search) {
                var d = document, elements, pattern, i, results = [];
                if ( d.querySelectorAll ) { // IE8
                    return d.querySelectorAll("." + search);
                }
                if ( d.evaluate ) { // IE6, IE7
                    pattern = ".//*[contains(concat(' ', @class, ' '), ' " + search + " ')]";
                    elements = d.evaluate(pattern, d, null, 0, null);
                    while ( (i = elements.iterateNext()) ) {
                        results.push(i);
                    }
                }
                else {
                    elements = d.getElementsByTagName("*");
                    pattern = new RegExp("(^|\\\\s)" + search + "(\\\\s|$)");
                    for ( i = 0; i < elements.length; i++ ) {
                        if ( pattern.test(elements[i].className) ) {
                            results.push(elements[i]);
                        }
                    }
                }
                return results;
            };
        }


        this.init();
    }


    window.Tabs = Tabs;


    var tabs = new Tabs(".tabs");


}(window, document));
[/spoiler]

Hi,

Finally got around to having a go at this as I have been away on holiday and my feeble attempt can be found here. It should work back to IE6 with any luck.

It’s basically a collection of snippets that I borrowed from around the web so can’t really take much credit although it did help broaden my knowledge a little (and to remind me how little I know about js).

There is a logic error in that it doesn’t take into account that there may be other tab elements on the page due to the fact that a class name was used and not an id. I did try to make a version that would run for multiple tab blocks but failed miserably so gave up as I ran out of time :slight_smile:

Thanks for making me have a try anyway and I look forward to seeing how the experts have done it.

So then, the deadline has passed and the competition is now officially closed.

Thank you very much to @fretburner ;, @sleignnet ;, @markbrown4 ;, @langsor ;, @dresden_phoenix ;, @AussieJohn ; & @Paul_O_B ; for some great entries.

Paul and my good self will go through the entries in the next few days and offer some feedback on what was submitted, as well as an “official” solution.

Watch this space, as they say …

Having now had time to look at the entries in more detail we’ve decided to first present our solution before announcing a winner.

We’d like to do this in stages, so that we can examine the various ways in which jQuery makes our life easier, as well as ways in which we can replicate what it does for us. Ideally this will lead people to question the wisdom of including a large library in a project when only a minor subset of its functionality is required, as well as highlight some current best practises along the way.

As ianc.sp points out, the main complexity of converting the tabbed-navigation into plain JavaScript is the AJAX request which loads the content into the page. Accordingly, this seems like a good place to start.

As you all know, AJAX requests are made using XMLHttpRequest (XHR). This is a protocol that was originally designed by Microsoft, implemented in JavaScript by Mozilla, Apple, and Google and is currently being standardized by the W3C.

In standards compliant browsers, you simply create an instance of the object, open a URL, and send the request. The HTTP status of the result, as well as the result’s content, is available in the request object once the transaction is completed.

So far, so good …

However, in old versions of Internet Explorer (IE5 and IE6) XMLHttpRequest is not supported, with these browsers relying on the proprietary ActiveXObject to make Ajax requests. To make matters worse, there are reports of XMLHttpRequest being buggy in IE7, too.

So what to do?

Instead of rolling your own AJAX solution, it can be better to stand on the shoulders of giants and make good use of solutions that have already been created. At the microjs.com site you can easily see all of the common libraries that handle it well. Some are large and provide additional functionality, while others such as [URL=“https://code.google.com/p/microajax/”]microAjax provide only the ajax functionality with no extraneous addons.

For example, microAjax gets the request with:


this.getRequest = function() {
  if (window.ActiveXObject)
    return new ActiveXObject('Microsoft.XMLHTTP');
  else if (window.XMLHttpRequest)
    return new XMLHttpRequest();
  return false;
};

The magic sauce that they have, is when it comes time to make the post, they check if you are posting any data with the request and supply the appropriate headers for that request.


if(this.request) {
  var req = this.request;
  req.onreadystatechange = this.bindFunction(this.stateChange, this);

  if (this.postBody!=="") {
    req.open("POST", url, true);
    req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    req.setRequestHeader('Connection', 'close');
  } else {
    req.open("GET", url, true);
  }

  req.send(this.postBody);
}

So what we have with microAjax is a simple set of code that easily achieves cross-browser compatibility, while ensuring that the standard needs of someone making an ajax request are met. It’s difficult to improve on that, thus this is what we have chosen to use.

If you do happen to require any additional special requirements though, there are other ajax libraries that may provide just what you need.

It’s also possible for you to take inspiration from any of the code, and to modify things to your own particular needs. Take care with that though, because it can be difficult to determine if such changes won’t work for others, even though they seem to work currently for you.

Having said all of that, at this juncture I would also encourage you to look at what jQuery is doing under the hood:

// Functions to create xhrs
function createStandardXHR() {
    try {
        return new window.XMLHttpRequest();
    } catch( e ) {}
}

function createActiveXHR() {
    try {
        return new window.ActiveXObject( "Microsoft.XMLHTTP" );
    } catch( e ) {}
}

// Create the request object
// (This is still attached to ajaxSettings for backward compatibility)
jQuery.ajaxSettings.xhr = window.ActiveXObject ?
    /* Microsoft failed to properly
     * implement the XMLHttpRequest in IE7 (can't request local files),
     * so we use the ActiveXObject when it is available
     * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
     * we need a fallback.
     */
    function() {
        return !this.isLocal && createStandardXHR() || createActiveXHR();
    } :
    // For all other browsers, use the standard XMLHttpRequest object
    createStandardXHR;

One way of doing this is to use James Padolsey’s jQuery source viewer.

So, that’s part one. Part two will follow shortly, but in the meantime we would welcome any comments or observations on the above.

Good explanation Pullo - look forward to part 2 :slight_smile:

One of the main reasons behind jQuery’s meteoric rise to popularity was its use of the CSS selector syntax to select DOM elements and the ease with which it could then traverse these and modify their content.

In this post I’d like to focus on how jQuery handles DOM selection under the hood and what native JavaScript functions we can use in its place.

A quick glance at the API docs, shows the syntax for jQuery DOM selection to be jQuery( selector [, context ] ).
The fact that this method takes an optional context parameter might come as a surprise to some, as we normally see it used without. However, passing a valid context parameter thus: $(‘a’, $nav), would be equivalent to calling $nav.find('a').

When jQuery receives a valid selector as a first parameter it starts by identifying the string it was passed using a regular expression. The easiest scenario here is when this string is an id, in which case jQuery can call the traditional document.getElementById and wrap the returned element in a jQuery object.

In most other cases, jQuery will leverage the following native JavaScript methods where they are available:

[LIST]
[]document.querySelectorAll(selector) — returns a non-live NodeList of all the matching element nodes
[
]document.getElementsByTagName(tagname) — returns a live HTMLCollection of matching elements ('live’meaning that it updates itself automatically to stay in sync with the DOM tree)
[*]document.getElementsByClassName(class) — returns a HTMLCollection of elements with a specific class name
[/LIST]Support for these methods is varied:

[LIST]
[]getElementById was introduced in DOM Level 1 for HTML documents and works well in almost all browsers (with the odd exception)
[
]getElementsByTagName was introduced in DOM Level 2 and also works well in almost all browsers (again with the odd exception).
[]document.getElementsByClassName is not as widely supported, most notably lacking support in IE8. Support overview.
[
]querySelectorAll and the related querySelector (which returns the first element within the document that matches the specified group of selectors) were introduced with the HTML5 Selectors API. They have been around for some time now and are supported in all modern browsers and IE8 (although IE8 only supports up to CSS2.1 selectors). [URL=“http://caniuse.com/queryselector”]Support overview.
[/LIST]For the case that native support is not available, jQuery falls back to using the Sizzle engine which was written by John Resig (author of jQuery).
Sizzle is a pure-JavaScript CSS selector engine designed to be easily dropped in to a host library. It’s lightweight (4kb when minified), easy to use and I encourage you to check it out.

E.g. to select all <span> elements which are a direct child of a <p> elements, you would do:

Sizzle('p > span');

or to select all inputs with an attribute name that starts with ‘news’:

Sizzle('input[name^="news"]')

You can find the Sizzle documentation here, where you will also find details on additional selectors that are supported by sizzle too.

You can also search the [jQuery"]jQuery sourcecode](http://code.jquery.com/jquery-latest.js) for occurrences of the term “Sizzle” to get an idea of how it is used.

So what did we use?
Again, why reinvent the wheel? We opted to include the Sizzle library in our code, then mapped querySelector and querySelectorAll to Sizzle if native browser support wasn’t detected.

You can do that, like this:

// If there's native support for querySelector, don't load Sizzle.
if (undefined !== document.querySelector) {
	return;
}

...

document.querySelectorAll = function querySelectorAll(selector){
	return Sizzle(selector, this);
};
document.querySelector = function querySelector(selector){
	return (document.querySelectorAll.call(this, selector)[0] || null);
};

There are a couple of caveats to this approach. For example, IE7 doesn’t have an Elements object from which to inherit things like querySelector, so you can’t do:

<ul id="myList">
  <li>One</li>
  <li>Two</li>
  <li class="highlight">Three</li>
  <li>Four</li>
  <li class="highlight">Five</li>
</ul>
var myList = document.getElementById("myList");
console.log(myList.querySelectorAll(".highlight").length);

In such a situation the obvious solution for compatibility is to refrain from using querySelector / querySelectorAll on partial searches, and use things like getElementsByTagName instead.


Another area I would like to cover are the jQuery helper functions hasClass(), addClass() and removeClass() which we had to convert to regular JavaScript in the course of this challenge.

Here’s our solution followed by a brief explination:

function hasClass (ele, cls) {
  return ele.className.match(new RegExp('(\\\\s|^)' + cls + '(\\\\s|$)'));
};

This uses a regular expression to match a class name which can be preceded or followed by optional whitespace (to allow for multiple class names on the same element).

function addClass (ele, cls) {
  if (!hasClass(ele, cls)) {
    ele.className += ' ' + cls;
  }
};

Hopefully this is a no brainer. Here we are using the previous helper function to check if the element already has a particular class name. If not we are adding it using string concatenation.

function removeClass (ele, cls) {
  if (hasClass(ele, cls)) {
    var reg = new RegExp('(\\\\s|^)' + cls + '(\\\\s|$)'),
        newClass = ele.className.replace(reg, ' ');
    ele.className = newClass.replace(/^\\s+|\\s+$/g, '');
  }
};

This is a little more complicated. We cannot simply subtract the class we wish to remove (as we could in a language like Ruby), as otherwise JavaScript will attempt to coerce the string to a number and this will result in your element getting a class of NaN (Not A number).
Instead we check if the element we want to remove the class from, actually has that class.
If it does we use a regular expression to replace that particular class in the element’s className property with a space, and assign the result to a variable newClass.
We then assign the value held in our newClass variable to the element’s className propery.

This technique (obviously) makes use of the recent classList interface that provides easy and fast manipulation of element class names, however Internet Explorer only supports it as of IE10. There is a polyfill that provides support back to IE8 and IE9, but if you’re needing things to work on IE7 or IE6 then you’re out of luck and when you attempt to use the .classList methods, the browser won’t like the undefined object errors.

So, much as we would love to recommend the normal use of classList, any attempts to use it currently still have to check if the property exists before using it, due to IE6 and IE7 issues, so some form of hasClass, addClass and removeClass function is still required.

As an end result, we can use Eli Grey’s classList shim that can be found on the classList documentation page to polyfill suport for IE8 and IE9, and we can access it via custom hasClass, addClass and removeClass functions to protect against classList not being supported. What we end up with is loading the polyfill followed by class handling functions:


<script src="js/polyfill/classlist-polyfill.js"></script>
<script src="js/class-handling.js"></script>

Because we do want to use the classList techniques if they are at all supported, the class-handling.js script chooses which one to use:


...
function nativeRemoveClass(el, className) {
    el.classList.remove(className);
}
var supportsClassList = !!document.body.classList;
window.hasClass = supportsClassList ? nativeHasClass : hasClass;
window.addClass = supportsClassList ? nativeAddClass : addClass;
window.removeClass = supportsClassList ? nativeRemoveClass : removeClass;

An example of the above can be seen in action at the bottom of the script, at http://jsfiddle.net/pmw57/B7SHE/

So, that’s it for now.
Well done if you made your way through all that :slight_smile:

By way of extra-curricular reading, here are one or two more great articles you might want to check out:

http://www.sitepoint.com/series/native-javascript-equivalents-of-jquery-methods/
http://blog.bigbinary.com/2010/02/15/how-jquery-selects-elements-using-sizzle.html
http://tech.pro/tutorial/1385/out-growing-jquery

Stay tuned for part three when Paul will present the nuts and bolts of our solution.

That’s a nice write-up. Looking forward to seeing how you guys have built your solution :slight_smile:

Thanks John.
I appreciate that!

Thanks Pullo - here’s what we’ve done with our solution.

Our approach

When writing code there are a number of different factors competing for your attention. If you want the code to be clear and understandable, and you want the code to also work in a cross-browser fashion, then it has to be accepted that this comes at the cost of size, due to polyfills and other-such techniques that allow the modern to work with the old.

If keeping the file size down was more important then the polyfills would have to be lost. This results in either a loss of cross-browser compatibility, or it means writing the code using older techniques that tend to be more difficult to understand.

So we have a choice between clarity, compatibility, and size - pick any two.

Because I’m not interested in writing unclear code, and because I try to maintain communication with older browsers, the code that we have come up with has been arrived at with an eye to the future by using modern techniques for clarity, and an eye to the past with polyfills for cross-browser compatibility.

Modern Techniques

We’ve chosen to use modern coding techniques where they can be still supported by older web browsers such as IE7, because for the most part such techniques help to make the code easier to understand. There are some common modern techniques though that we haven’t used. So here’s a rundown of them, and why.

Commonly forEach is one of the first choices for a polyfill, but in our code there is no use for that due to it being a nodelist that we are iterating over. If we did have an array to use forEach on, the compatibility code to provide full and complete support for forEach is quite large, so we could instead use of a smaller set of compatibility code that does a good enough job, and has the potential to be updated later on if needed should we require the additional support.

The classList object is used in our code, but only at arms length through functions such as addClass. Due to issues with IE7 we can’t make calls to el.classList without causing errors in IE7, so even when using a polyfill for it, we still have to check first whether classList is supported before being able to use it. If we only cared about supporting back to IE8, then with the classList polyfill we can use el.classList directly in our code without any issues.

While querySelector is used to retrieve elements that the document.getElementById can be use for, there are other forms of retrieval that we use querySelector with such as obtaining the tab anchors, so we gain a good benefit by consistently using querySelector throughout our code.

Polyfills

Polyfills are additional pieces of scripting code that check if a web browser is capable of supporting a certain feature, and if not then additional code is used to emulate that missing feature instead. This is usually effective for extending support of features to web browsers that are several versions earlier than those that natively support a feature.

One of the added benefits of using polyfills is that they can at some later stage be removed without impacting your code. Because the polyfill allows you to use the modern technique, the only concern when removing a polyfill is in terms of the older web browsers that you don’t care to continue to support any more.

querySelector

The can I use web site is very useful for determining native support of features, and older support when polyfills are used. For example, their page on [url=“http://caniuse.com/queryselector”]querySelector support shows that Internet Explorer has supported that natively since IE8, and when you hover on the red indicators for IE6 and IE7 you’re shown that it’s not supported but polyfills are available. Sometimes the page will provide a reference link to a polyfill that can be used, but in this case one is not provided so a google search for [FONT=Courier New][URL=“https://www.google.com/search?q=javascript+queryselector+polyfill”]javascript queryselector polyfill[/FONT] can be done to allow us to research potential solutions.

In this case, there are a couple of good querySelector polyfills to pick from:

[list=1][]GitHub of polyfills is where you can extract from polyfill.js the part for querySelector, which links to its original article [url=“http://ajaxian.com/archives/creating-a-queryselector-for-ie-that-runs-at-native-speed”]Creating a querySelector for IE that runs at “native speed”
[
]https://gist.github.com/branneman/1200441 is quite a different querySelector polyfill that’s much larger in size because it incorporates the Sizzle CSS selector engine.[/list]
The first one with its small size would be preferable, but it doesn’t seem to work on IE7 in the way that we are using it, so the second polyfill with the built-in sizzle engine is what we’ll be using here.

addEventListener

It’s tempting to go with the MDN compatibility code for addEventListener but as it’s only good for IE8, further looking reveals an MIT student who has provided a polyfill good back to IE6, at https://gist.github.com/eirikbacker/2864711 - this is a very nice benefit, because we can now happily use addEventListener in our code without any further compatibility issues.

classList

Working with class names has always been tricky and commonly results in us using some class-handling functions that provide much of the functionality for us. Thanks to the classList interface though, we now have the native ability to work with class names. Because it’s the newest feature of what we’re using though, browser compatibility is a real issue. At this stage if we were to use it as designed, even with a polyfill, IE7 would throw an error. We could go back to those class-handling functions from before, but then we wouldn’t gain any benefit from browsers that do support classList. So a hybrid solution is called for.
Eli Grey has a nice classList polyfill that’s good back to IE8. If we want to go back further than that though, we’ll have to come up with a hybrid solution, which is where helper function come in to play.

Helper functions

Class-handling functions

We are going to be using the classList method where it is supported, and when it’s not we will be reverting back to using the class-handling functions. How we do that, is by having the functions defined separately, and then choosing which ones to use for hasClass, addClass, removeClass when things begin.


if (supportsClassList) {
    window.hasClass = classListContains;
    window.addClass = classListAdd;
    window.removeClass = classListRemove;
} else {
    window.hasClass = hasClass;
    window.addClass = addClass;
    window.removeClass = removeClass;
}

MicroAjax
The ajax communication library that we are using is a tiny one called MicroAjax. How we found is is by searching the [URL=“http://microjs.com/”]microjs web site (no relation to MicroAjax) for their smallest ajax library, which does all that we need to do here.

Fade in/out

The last of what could be called some helper functions are for fading the content in and out. In this case, we are just adding a class name of fade-out to fade things out, and after a small delay we replace that with fade-in to fade things in. The fading effect is achieved with CSS. For example:


#content { /* vendor prefixes are thanks to http://cssprefixer.appspot.com/ */
    -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;
}
#content.fade-out {
    zoom: 1;
    filter: alpha(opacity=0);
    opacity: 0;
}
#content.fade-in {
    zoom: 1;
    filter: alpha(opacity=100);
    opacity: 1;
}

Even though IE9 and below don’t perform the fading effect, their content still goes away for a short while before being replaced with the new content, which helps to provide a visual-feedback that the content has changed.

Conclusion

So after having explained what has been done, the next post will provide all of the details for the code itself. In the meantime though if you’re curious, you can download the vanilla version of the code from this dropbox location.