Unobtrusive JavaScript without jQuery
I was recently required to create a simple fadeIn/fadeOut hover script for a client, and I opted to use my favourite technique: the jQuery library. However, my client was uncomfortable with all the extra functionality that comes with jQuery, and I couldn't convince them that a minified/gzipped/cached jQuery script was good enough, just on the principal that a small custom minified/gzipped/cached script would be even better. At first I decided that it would be fairly easy to check the jQuery source out of SVN and compile a smaller version without all the functions that I didn't need. Recompiling a custom version of jQuery proved to be more difficult than I thought and I encountered problem after problem trying to cut out irrelevant files and code. In the end I decided that I would try to build something like jQuery from the ground up.
JavaScript veterans may find a lot of this to be old and trivial, but I could not find anywhere on the net that gave this information in a succinct and straight forward manner that encouraged developers to give it a go, so I've decided to write up this quick guide on getting started with creating your own script that does the equivalent of what jQuery does without downloading handling for every kind of selector, attribute, traversing, manipulation, css, event, effect, ajax, and utility. I'm not too concerned about the syntax being similar to jQuery either, just the ability to do the same sort of thing.
What will we need to create something like a jQuery core?
A $(document).ready() function equivalent
First thing we need with most jQuery style scripts is the ability to create one or more blocks of code that will be executed when the DOM is ready. jQuery uses one or more $(document).ready() functions to run unobstrusive scripts. In the JavaScript community this sort of function is called an 'addEvent' function, and you can Google 'addEvent' to find the latest efforts that javascript developers have made on creating such a function, including a contest quirksmode.org held back in 2005 that was won by jQuery creator John Resig. The aftermath of that contest saw many improvements, including the addEvent function that I personally choose to use: Rock Solid addEvent() by Dustin Diaz. To use it, just add this piece of code at the bottom of your script:
addEvent(window, "load", myReadyFunction);
This will call the function 'myReadyFunction()' when the document is ready. You can add this line of code several times, and each time make it call a different function, thus having the equivalent of multiple $(document).ready() functions.
You can also pass in a reference to a DOM element (as returned from a function like getElementById()) instead of window, and then that element reference will be available within the called function as the keyword this. And, instead of “load” you can pass in events like “mouseover” or “click”. This is how you would bind events to an element.
But getting element references using getElementById() is a bit ghetto, and will be unsuitable for a lot of people used to working with jQuery. So we'll need something better than that.
If you want to make it even more like jQuery you could look into copying jQuery's bindReady() function.
A selector equivalent
jQuery gives you a lot of options for how to supply a string to identify some set of elements to affect. You might not need the ability to select things by attribute, or by form element type, or using filters, so there are a few different choices you could pursue here. In my case I only needed to select elements of a particular Tag Name, Class, and within a particular container element.
Here are some options you could look into.
- GetElementsBy* - a website listing various functions people have made to fetch elements.
- Sizzle – this is the selector engine that jQuery uses. This is much more functionality than I needed though.
- Or you can Google 'javascript css selector' and such to get the latest news regarding CSS selector scripts. A lot of browsers are even implementing more types of selector handling by default so there may be better techniques that come along to take advantage of that.
I found that getElementsByClassName Deluxe Edition by Stuart Colville was perfectly appropriate for my needs.
Binding events to DOM elements
Selecting elements and then binding functions to particular events is a simple matter of putting the addEvent functionality together with the selector function.
Here is an example of how to select elements “li.list-item” and binding mouseover and mouseout to them. Not shown in this code is the addEvent() stuff or the getElementsByClassName function, nor have I included the custom functions listFadeIn() and listFadeOut() which can access the affected element with the keyword this.
// Ready function
function listReady() {
var items = getElementsByClassName("list-item", "li");
for (var item in items) {
addEvent(items[item], "mouseover", listFadeIn);
addEvent(items[item], "mouseout", listFadeOut);
}
}
// Add the load event.
addEvent(window, "load", listReady);
Creating the effects or coding what you want to happen
Well this is really the easy part to explain, but where you will spend most of the time on your script, as you can look up any crappy old script that normally would have used inline JavaScript to get executed, and rewrite it for your own purposes. Just Google the effect (or whatever) you need to happen, and you're sure to find someone that has done it before.
For example, I needed to create the fadeIn/fadeOut script, one of the first results on Google is this: Cross-browser BlendTrans Filter JavaScript from Brainerror.net. This gave me everything I needed to figure out how to do a similar thing on top of my own small faux jQuery-like script. You can see it in the example below.
An example script
Here is an example script. I've done a bit more work on it than explained above. I've added some code to my 'ready' function to switch out png backgrounds for gifs in Internet Explorer because of the problem with using opacity with 24 bit pngs. This is probably irrelevant to you. There is a hack in there to change the behaviour of the setTimeout function in Internet Explorer to accept additional parameters like other browsers do (Simple setTimeout / setInterval extra arguments IE fix) . Finally, due to event bubbling I've also had to add code to the addEvent function to add the mouseenter and mouseleave functions to non-IE browsers as they proved to create no 'flicker' as mouseover and mouseout did in Firefox. (Mouseenter and mouseleave events for Firefox (and other non-IE browsers)). You will also notice I've added a setTimeout to my FadeIn and a clearTimeout to my FadeOut – this is just a quick way of simulating hoverIntent without jQuery.
/**
* domCloak
*
* Date: 18 August 2009.
* Author: D Braksator.
*/
// Override IE setTimeout.
/*@cc_on
(function(f){
window.setTimeout =f(window.setTimeout);
window.setInterval =f(window.setInterval);
})(function(f){return function(c,t){var a=[].slice.call(arguments,2);return f(function(){c.apply(this,a)},t)}});
@*/
// Event caching
var domCloakEventCache = function(){
var listEvents = [];
return {
listEvents : listEvents,
add : function(node, sEventName, fHandler){
listEvents.push(arguments);
},
flush : function(){
var i, item;
for(i = listEvents.length - 1; i >= 0; i = i - 1){
item = listEvents[i];
if(item[0].removeEventListener){
item[0].removeEventListener(item[1], item[2], item[3]);
}
if(item[1].substring(0, 2) != "on"){
item[1] = "on" + item[1];
}
if(item[0].detachEvent){
item[0].detachEvent(item[1], item[2]);
}
item[0][item[1]] = null;
}
}
};
}();
// Mouse enter function
function domCloakMouseEnter(fn) {
return function(event) {
var relTarget = event.relatedTarget;
if (this === relTarget || domCloakChildOf(this, relTarget)) { return; }
fn.call(this, event);
}
};
// Is 'parent' a child of 'child'?
function domCloakChildOf(parent, child) {
if (parent === child) { return false; }
while (child && child !== parent) { child = child.parentNode; }
return child === parent;
}
// Add DOM event
function domCloakAddEvent(obj, type, fn) {
if (obj.addEventListener) {
if (type == 'mouseenter') {
type = 'mouseover';
fn = domCloakMouseEnter(fn);
}
else if (type == 'mouseleave') {
type = 'mouseout'
fn = domCloakMouseEnter(fn);
}
obj.addEventListener(type, fn, false);
domCloakEventCache.add(obj, type, fn);
}
else if (obj.attachEvent) {
obj["e" + type + fn] = fn;
obj[type + fn] = function() {
obj["e" + type + fn](window.event);
}
obj.attachEvent("on" + type, obj[type + fn]);
domCloakEventCache.add(obj, type, fn);
}
else {
obj["on" + type] = obj["e" + type + fn];
}
}
// Class selector
function domCloakSelector(strClass, strTag, objContElm) {
strTag = strTag || "*";
objContElm = objContElm || document;
var objColl = objContElm.getElementsByTagName(strTag);
if (!objColl.length && strTag == "*" && objContElm.all) objColl = objContElm.all;
var arr = new Array();
var delim = strClass.indexOf('|') != -1 ? '|' : ' ';
var arrClass = strClass.split(delim);
for (var i = 0, j = objColl.length; i < j; i++) {
var arrObjClass = objColl[i].className.split(' ');
if (delim == ' ' && arrClass.length > arrObjClass.length) continue;
var c = 0;
comparisonLoop:
for (var k = 0, l = arrObjClass.length; k < l; k++) {
for (var m = 0, n = arrClass.length; m < n; m++) {
if (arrClass[m] == arrObjClass[k]) c++;
if ((delim == '|' && c == 1) || (delim == ' ' && c == arrClass.length)) {
arr.push(objColl[i]);
break comparisonLoop;
}
}
}
}
return arr;
}
// Opacity function
function domCloakOpacity(opacity, element) {
var fraction = opacity / 100;
// Standards
element.style.opacity = fraction;
// Old mozilla
element.style.MozOpacity = fraction;
// Safari / KHTML
element.style.KhtmlOpacity = fraction;
// Explorer
element.style.filter = "alpha(opacity=" + opacity + ")";
}
// Fader function
function domCloakFade(element, state, speed) {
timer = 0;
opacity = (state == 1) ? 0 : 100;
end = (state == 1) ? 101 : -1;
while (opacity != end) {
setTimeout(function (opacity, state) {
domCloakOpacity(opacity, element);
if (opacity < 1) {
element.style.display = (state == 1) ? "block" : "none";
}
}, (timer * speed), opacity, state);
timer++;
(state == 1) ? opacity++ : opacity--;
}
}
// FadeIn function
function domCloakFadeIn() {
var items = domCloakSelector("list-tip-1", "span", this);
for (var item in items) {
items[item].domCloakTimer = setTimeout(function () {
domCloakFade(items[item], 1, 3);
}, 300);
}
}
// FadeOut function
function domCloakFadeOut() {
var items = domCloakSelector("list-tip-1", "span", this);
for (var item in items) {
clearTimeout(items[item].domCloakTimer);
domCloakFade(items[item], 0, 1);
}
}
// Ready function
function domCloakReady() {
if (navigator.appName == 'Microsoft Internet Explorer') {
var items = domCloakSelector("list-tip-1|list-tip-2|list-tip-3|list-tip-4", "span");
for (var item in items) {
items[item].style.backgroundImage = items[item].currentStyle.backgroundImage.replace('.png', '.gif');
}
}
var items = domCloakSelector("list-item", "li");
for (var item in items) {
domCloakAddEvent(items[item], "mouseenter", domCloakFadeIn);
domCloakAddEvent(items[item], "mouseleave", domCloakFadeOut);
}
}
// Add the load and unload events.
domCloakAddEvent(window, "unload", domCloakEventCache.flush);
domCloakAddEvent(window, "load", domCloakReady);
