Build Your First JavaScript Library

Build Your First JavaScript Library

Tutorial Details
    • Difficulty: Intermediate
    • Completion Time: 30 minutes

Ever marvelled at the magic of Mootools? Ever wondered how Dojo does it? Ever been curious about jQuery’s gymnastics? In this tutorial, we’re going to sneak behind the scenes and try our hand at building a super-simple version of your favorite library.

We use JavaScript libraries nearly every day. When you’re just getting started, having something like jQuery is fantastic, mainly because of the DOM. First, the DOM can be rather rough to wrangle for a beginner; it’s a pretty poor excuse for an API. Secondly, it’s not even consistent across all browsers.

We’re wrapping the elements in an object because we want to be able to create methods for the object.

In this tutorial, we’re going to take a (decidedly shallow) stab at building one of these libraries from scratch. Yes, it’ll be fun, but before you get too excited, let me clarify a few points:

  • This won’t be a completely full-featured library. Oh, we’ve got a solid set of methods to write, but it’s no jQuery. We’ll do enough to give you a good feeling for the kind of problems that you’ll run into when building libraries.
  • We aren’t going for complete browser compatibility across the board here. What we’re writing today should work on Internet Explorer 8+, Firefox 5+, Opera 10+, Chrome, and Safari.
  • We aren’t going to cover every possible use of our library. For example, our append and prepend methods will only work if you pass them an instance of our library; they won’t work with raw DOM nodes or nodelists.

One more thing: while we won’t be writing tests for this library, I did that when first developing this. You can get the library and tests on Github.


Step 1: Creating the Library Boilerplate

We’ll start with some wrapper code, which will contain our whole library. It’s your typical immediately invoked function expression (IIFE).

window.dome = (function () {
	function Dome (els) {
		
	}
	
	var dome = {
		get: function (selector) {
		
		}	
	};
	
	return dome;
}());

As you can see, we’re calling our library Dome, because it’s primarily a DOM library. Yes, it’s lame.

We’ve got a couple of things going on here. First, we have a function; it will eventually be a constructor function for the instances of our library; those objects will wrap our selected or created elements.

Then, we have our dome object, which is our actual library object; as you can see, it’s returned at the end there. It’s got an empty get function, which we’ll use to select elements from the page. So, let’s fill that in now.


Step 2: Getting Elements

The dome.get function will take one parameter, but it could be a number of things. If it’s a string, we’ll assume it’s a CSS selector; but we can also take a single DOM Node, or a NodeList.

get: function (selector) {
	var els;
	if (typeof selector === "string") {
    	els = document.querySelectorAll(selector);
	} else if (selector.length) {
		els = selector;
	} else {
		els = [selector];
	}
	return new Dome(els);
}

We’re using document.querySelectorAll to simplify the finding of elements: of course, this does limit our browser support, but for this case, that’s okay. If selector is not a string, we’ll check for a length property. If it exists, we’ll know we have a NodeList; otherwise, we have a single element and we’ll put that in an array. That’s because we need an array to pass to our call to Dome at the bottom there; as you can see, we’re returning a new Dome object. So let’s go back to that empty Dome function and fill it in.


Step 3: Creating Dome Instances

Here’s that Dome function:

function Dome (els) {
	for(var i = 0; i < els.length; i++ ) {
		this[i] = els[i];
	}
	this.length = els.length;
}

I really recommend you dig around inside a few of your favourite libraries.

This is really simple: we just iterate over the elements we selected and stick them onto the new object with numeric indices. Then, we add a length property.

But what’s the point here? Why not just return the elements? We’re wrapping the elements in an object because we want to be able to create methods for the object; these are the methods that will allow us to interact with those elements. This is actually a boiled-down version of the way jQuery does it.

So, now that we have our Dome object being returned, let’s add some methods to its prototype. I’m going to put those methods right under the Dome function.


Step 4: Adding a Few Utilities

The first functions we’re going to write are simple utility functions. Since our Dome objects could wrap more than one DOM element, we’re going to need to loop over every element in pretty much every method; so, these utilities will be handy.

Let’s start with a map function:

Dome.prototype.map = function (callback) {
	var results = [], i = 0;
	for ( ; i < this.length; i++) {
		results.push(callback.call(this, this[i], i));
	}
	return results;
};

Of course, the map function takes a single parameter, a callback function. We’ll loop over the items in the array, collecting whatever is returned from the callback in the results array. Notice how we’re calling that callback function:

callback.call(this, this[i], i));

By doing it this way, the function will be called in the context of our Dome instance, and it will receive two parameters: the current element, and the index number.

We also want a forEach function. This is actually really simple:

Dome.prototype.forEach(callback) {
	this.map(callback);
	return this;
};

Since the only difference between map and forEach is that map needs to return something, we can just pass our callback to this.map and ignore the returned array; instead, we’ll return this to make our library chainable. We’ll be using forEach quite a bit. So, notice that when we return our this.forEach call from a function, we’re actually returning this. For example, these methods actually return the same thing:

Dome.prototype.someMethod1 = function (callback) {
	this.forEach(callback);
	return this;
};

Dome.prototype.someMethod2 = function (callback) {
	return this.forEach(callback);
};

One more: mapOne. It’s easy to see what this function does, but the real question is, why do we need it? This requires a bit of what you could call “library philosophy.”

A Short “Philosophical” Detour

Firstly, the DOM can be rather rough to wrangle for a beginner; it’s a pretty poor excuse for an API.

If building a library were just about writing the code, it wouldn’t be too difficult a job. But as I worked on this project, I found the tougher part was deciding how certain methods should work.

Soon, we’re going to build a text method that returns the text of our selected elements. If our Dome object wraps several DOM node (dome.get("li"), for example), what should this return? If you do something similar in jQuery ($("li").text()), you’ll get a single string with the text of all the elements concatenated together. Is this useful? I don’t think so, but I’m not sure what a better return value would be.

For this project, I’ll return the text of multiple elements as an array, unless there’s only one item in the array; then we’ll just return the text string, not an array with a single item. I think you’ll most often be getting the text of a single element, so we optimize for that case. However, if you’re getting the text of multiple elements, we’ll return something you can work with.

Back to Coding

So, the mapOne method will simply run map, and then either return the array, or the single item that was in the array. If you’re still not sure how this is useful, stick around: you’ll see!

Dome.prototype.mapOne = function (callback) {
	var m = this.map(callback);
	return m.length > 1 ? m : m[0];
};

Step 5: Working with Text and HTML

Next, let’s add that text method. Just like jQuery, we can pass it a string and set the element’s text, or use no parameters to get the text back.

Dome.prototype.text = function (text) {
	if (typeof text !== "undefined") {
		return this.forEach(function (el) {
			el.innerText = text;
		});
	} else {
		return this.mapOne(function (el) {
			return el.innerText;
		});
	}
};

As you might expect, we need to check for a value in text to see if we’re setting or getting. Note that just if (text) wouldn’t work, because an empty string is a false value.

If we’re setting, we’ll do a forEach over the elements and set their innerText property to the text. If we’re getting, we’ll return the elements’ innerText property. Note our use of the mapOne method: if we’re working with multiple elements, this will return an array; otherwise, it will be just the string.

The html method will do pretty much the same thing as text, except that it will use the innerHTML property, instead of innerText.

Dome.prototype.html = function (html) {
	if (typeof html !== "undefined") {
		this.forEach(function (el) {
			el.innerHTML = html;
		});
		return this;
	} else {
		return this.mapOne(function (el) {
			return el.innerHTML;
		});
	}
};

Like I said: almost identical.


Step 6: Hacking Classes

Next up, we want to be able to add and remove classes; so let’s write the addClass and removeClass methods.

Our addClass method will take either a string or an array of class names. To make this work, we need to check the type of that parameter. If it’s an array, we’ll loop over it and create a string of class names. Otherwise, we’ll just add a single space to the front of the class name, so it doesn’t mess with the existing classes on the element. Then, we just loop over the elements and append the new classes to the className property.

Dome.prototype.addClass = function (classes) {
	var className = "";
	if (typeof classes !== "string") {
		for (var i = 0; i < classes.length; i++) {
			className += " " + classes[i];
		}
	} else {
		className = " " + classes;
	}
	return this.forEach(function (el) {
		el.className += className;
	});
};

Pretty straightforward, eh?

Now, what about removing classes? To keep it simple, we’ll only allow removing one class at a time.

Dome.prototype.removeClass = function (clazz) {
	return this.forEach(function (el) {
		var cs = el.className.split(" "), i;

		while ( (i = cs.indexOf(clazz)) > -1) { 
			cs = cs.slice(0, i).concat(cs.slice(++i));
		}
		el.className = cs.join(" ");
	}); 
}; 

On every element, we’ll split the el.className into an array. Then, we use a while loop to slice out the offending class until cs.indexOf(clazz) returns -1. We do this to cover the edge case where the same classes has been added to an element more than once: we need to make sure it’s really gone. Once we’re sure we’ve cut out every instance of the class, we join the array with spaces and set it on el.className.


Step 7: Fixing an IE Bug

The worst browser we’re dealing is IE8. In our little library, there’s only one IE bug that we need to deal with; thankfully, it’s pretty simple. IE8 doesn’t support the Array method indexOf; we use it in removeClass, so let’s polyfill it:

if (typeof Array.prototype.indexOf !== "function") {
	Array.prototype.indexOf = function (item) {
		for(var i = 0; i < this.length; i++) {
			if (this[i] === item) {
				return i;
			}
		}
		return -1;
	};
}

It’s pretty simple, and it’s not a full implementation (doesn’t support the second parameter), but it will work for our purposes.


Step 8: Adjusting Attributes

Now, we want an attr function. This’ll be easy, because it’s practically identical to our text or html methods. Like those methods, we’ll be able to both get and set attributes: we’ll take an attribute name and value to set, and just an attribute name to get.

Dome.prototype.attr = function (attr, val) {
	if (typeof val !== "undefined") {
		return this.forEach(function(el) {
			el.setAttribute(attr, val);
		});
	} else {
		return this.mapOne(function (el) {
			return el.getAttribute(attr);
		});
	}
};

If the val has a value, we’ll loop through the elements and set the selected attribute with that value, using the element’s setAttribute method. Otherwise, we’ll use mapOne to return that attribute via the getAttribute method.


Step 9: Creating Elements

We should be able to create new elements, like any good library can. Of course, this would be no good as a method on a Dome instance, so let’s put it right on our dome object.

var dome = {
	// get method here
	create: function (tagName, attrs) {

	}
};

As you can see, we’ll take two parameters: the name of the element, and an object of attributes. Most of the attributes be applied via our attr method, but two will get special treatment. We’ll use the addClass method for the className property, and the text method for the text property. Of course, we’ll need to create the element and the Dome object first. Here’s all that in action:

create: function (tagName, attrs) {
	var el = new Dome([document.createElement(tagName)]);
		if (attrs) {
			if (attrs.className) {
				el.addClass(attrs.className);
				delete attrs.className;
			}
		if (attrs.text) {
			el.text(attrs.text);
			delete attrs.text;
		}
		for (var key in attrs) {
			if (attrs.hasOwnProperty(key)) {
				el.attr(key, attrs[key]);
			}
		}
	}
	return el;
}

As you can see, we create the element and send it right into a new Dome object. Then, we deal with the attributes. Notice that we have to delete the className and text attributes after working with them. This keeps them from being applied as attributes when we loop over the rest of the keys in attrs. Of course, we end by returning the new Dome object.

But now that we’re creating new elements, we’ll want to insert them into the DOM, right?


Step 10: Appending and Prepending Elements

Next up, we’ll write append and prepend methods, Now, these are actually a bit tricky functions to write, mainly because of the multiple use cases. Here’s what we want to be able to do:

dome1.append(dome2);
dome1.prepend(dome2);

The worst browser we’re dealing is IE8.

The use cases are as these: we might want to append or prepend

  • one new element to one or more existing elements.
  • multiple new elements to one or more existing element.
  • one existing element to one or more existing elements.
  • multiple existing elements to one or more existing elements.

Note: I’m using “new” to mean elements not yet in the DOM; existing elements are already in the DOM.

Let’s step though it now:

Dome.prototype.append = function (els) {
	this.forEach(function (parEl, i) {
		els.forEach(function (childEl) {
		
		});
	});
};

We expect that els parameter to be a Dome object. A complete DOM library would accept this as a node or nodelist, but we won’t do that. We have to loop over each of our elements, and then inside that, we loop over each of the elements we want to append.

If we’re appending the els to more than one element, we need to clone them. However, we don’t want to clone the nodes the first time they’re appended, only subsequent times. So we’ll do this:

if (i > 0) {
	childEl = childEl.cloneNode(true);
}

That i comes from the outer forEach loop: it’s the index of the current parent element. If we aren’t appending to the first parent element, we’ll clone the node. This way, the actual node will go in the first parent node, and every other parent will get a copy. This works well, because the Dome object that was passed in as an argument will only have the original (uncloned) nodes. So, if we’re only appending a single element to a single element, all the nodes involved will be part of their respective Dome objects.

Finally, we’ll actually append the element:

parEl.appendChild(childEl);

So, altogether, this is what we have:

Dome.prototype.append = function (els) {
	return this.forEach(function (parEl, i) {
		els.forEach(function (childEl) {
			if (i > 0) {
				childEl = childEl.cloneNode(true); 
			}
			parEl.appendChild(childEl);
		}); 
	}); 
};

The prepend Method

We want to cover the same cases for the prepend method, so the method is pretty very similar:

Dome.prototype.prepend = function (els) {
	return this.forEach(function (parEl, i) {
		for (var j = els.length -1; j > -1; j--) {
			childEl = (i > 0) ? els[j].cloneNode(true) : els[j];
			parEl.insertBefore(childEl, parEl.firstChild);
		}
	}); 
};

The different when prepending is that if you sequentially prepend a list of elements to another element, they’ll end up in reverse order. Since we can’t forEach backwards, I’m going through the loop backwards with a for loop. Again, we’ll clone the node if this isn’t the first parent we’re appending to.


Step 11: Removing Nodes

For our last node manipulation method, we want to be able to remove nodes from the DOM. Easy, really:

Dome.prototype.remove = function () {
	return this.forEach(function (el) {
		return el.parentNode.removeChild(el);
	});
};

Just iterate through the nodes and call the removeChild method on each element’s parentNode. The beauty here (all thanks to the DOM) is that this Dome object will still work fine; we can use any method we want on it, including appending or prepending it back into the DOM. Nice, eh?


Step 12: Working with Events

Last, but certainly not least, we’re going to write a few functions for event handlers.

As you probably know, IE8 uses the old IE events, so we’ll have to check for that. Also, we’ll throw in the DOM 0 events, just ‘cause we can.

Check out the method, and then we’ll discuss it:

Dome.prototype.on = (function () {
	if (document.addEventListener) {
		return function (evt, fn) {
			return this.forEach(function (el) {
				el.addEventListener(evt, fn, false);
			});
		};
	} else if (document.attachEvent)  {
		return function (evt, fn) {
			return this.forEach(function (el) {
				el.attachEvent("on" + evt, fn);
			});
		};
	} else {
		return function (evt, fn) {
			return this.forEach(function (el) {
				el["on" + evt] = fn;
			});
		};
	}
}());

Here, we have an IIFE, and inside it we’re doing feature checking. If document.addEventListener exists, we’ll use that; otherwise, we’ll check for document.attachEvent or fall back to DOM 0 events. Notice how we’re returning the final function from the IIFE: that’s what will end up being assigned to Dome.prototype.on. When doing feature detection, it’s really handy to be able to assign the appropriate function like this, instead of checking for the features each time the function is run.

The off function, which unhooks event handlers, is pretty much identical:

Dome.prototype.off = (function () {
	if (document.removeEventListener) {
		return function (evt, fn) {
			return this.forEach(function (el) {
				el.removeEventListener(evt, fn, false);
			});
		};
	} else if (document.detachEvent)  {
		return function (evt, fn) {
			return this.forEach(function (el) {
				el.detachEvent("on" + evt, fn);
			});
		};
	} else {
		return function (evt, fn) {
			return this.forEach(function (el) {
				el["on" + evt] = null;
			});
		};
	}
}());

That’s It!

I hope you give our little library a try, and maybe even extend it a bit. Like I mentioned earlier, I have it up on Github, along with the Jasmine test suite for the code we’re written above. Feel free to fork it, play around, and send a pull request.

Let me clarify again: the point of this tutorial isn’t to suggest that you should always be writing your own libraries.

There are dedicated teams of people working together to make the big, established libraries as good as possible. The point here was to give a small peek into what might go on inside a library; I hope you’ve picked up a few tips here.

I really recommend you dig around inside a few of your favourite libraries. You’ll find that they aren’t so cryptic as you might have thought, and you’ll probably learn a lot. Here are a few great places to start:

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • https://timshomepage.net Timothy Warren

    So, why innerText over textContent? Besides IE 8, textContent is better supported, last I knew.

    • http://andrewburgess.ca Andrew Burgess
      Author

      Hm, good point; I really should have used textContent instead. In fact, that would have been a good example of checking for features, like we did with the events. A better text method would go something like this.

      Dome.prototype.text = function (text) {
      if (typeof text !== "undefined") {
      return this.forEach(function (el) {
      el[ (el.textContent) ? "textContent" : "innerText"] = text;
      });
      } else {
      return this.mapOne(function (el) {
      return el.textContent || el.innerText;
      });
      }
      };

      This will support IE8+, as well as Firefox (and the others, of course).

      • Tony Brown

        so update the tutorial?

      • http://margaine.com Florian Margaine

        This function won’t have the same behavior on IE8 and other browsers though :-) (quick google about innertext textContent shows this: http://clubajax.org/plain-text-vs-innertext-vs-textcontent/).

        jQuery uses nodeValue on nodes, it’s basically the only cross-browser way to get the same text in different browsers.

        I know it doesn’t matter so much for this tutorial, but I think it’s worth pointing out.

      • http://blog.pathtosharepoint.com Christophe

        +1 for textContent. Wouldn’t it make more sense to check for the existence of textContent before defining the function (rather than doing the check every time the function is called)?

  • http://github.com/KevinMartin/direct.js Kevin Jose Martin

    Great article! I recently made something called Direct.js and it follows some of your structure. I actually used jQuery’s implementation of Event Handling.

    I don’t know if I can link over (if not, just click on my name), but check out how I implemented onReady and maybe the rest of the script over at http://github.com/KevinMartin/direct.js

  • Rasmus Fløe

    function getIndex (array, item) {
    for(var i = 0; i < array.length; i++) {
    if (array[i] === item) {
    return i;
    }
    }
    return -1;
    }

    • Rasmus Fløe

      …oh great – I accidentally submitted the comment before being done :(

      But anyways

      I love ES5 shims – but they have to be spec compliant! Shimming Array::indexOf incorrectly may throw a spanner into your page; other scripts that may rely on the spec ‘ed behavior will break.

      Better just go with a simple function like the prematurely submitted one above :D

      Otherwise – nice article.

  • Jesus Bejarano

    Waow i was expecting too see some Regex, but this was a realy good tutorial.

  • Reynaldo

    Very cool!

  • http://blog.cartondonofriopartners.com K.C. Hunter

    Is there an advantage to writing javascript not as classes. I often prefer to a) write in jquery and b) use external files as classes ie.

    var MN = MN || {};

    MN.init = function()
    {
    console.log(‘start main class’);
    }

    • danjessen

      Because the IIFE or self invoking function as their also known give you closure. Which your method doesnt:

      var app = app || {};
      (function(exports, $, undefined) {
      var private_fn = function() {
      console.log(“from private function”);
      },
      public_fn = function() {
      console.log(“from public function”);
      };

      exports.public_fn = public_fn;
      })(app, jQuery)

  • http://www.tricktodesign.com tricktodesign

    Actullly thanks for this Javascript Library tutorial. i will definitely try my hands on it….

  • http://goo.gl/K9dEc Nic da Costa

    Great article! Thanks!

    I just have one question, is there any particular reason why you are not passing through window or at least document in your IIFE so that you are working with a local instance to slightly improve performance and the resolution process ( global vs local )? As document is referenced continuously throughout the code ( which is of course expected ).

    current structure:

    window.dome = ( function () {
    /* … code goes here … */
    }());

    as opposed to

    window.dome = ( function ( window , document ) {
    /* … code goes here … */
    }( window , document ));

    Just curious is all. Otherwise really great article! Glad to see tutorials such as this being written!

    • http://margaine.com Florian Margaine

      Probably because the advantage is seriously negligible. Besides, it’s not a “let’s build jquery” tutorial, but rather a simplistic tutorial :-)

    • http://andrewburgess.ca Andrew Burgess
      Author

      No good reason :). Making document local via the arguments would be good improvement!

  • http://margaine.com Florian Margaine

    I wonder, if you shimmed indexOf, why you didn’t shim a lot of other stuff? Such as classList, forEach, map, addEventListener, etc.

    A library like this would still be useful, as calling [].forEach.call( els, fn ); is worse than dome( selector ).forEach( fn );.

    • http://andrewburgess.ca Andrew Burgess
      Author

      I shimmed what was necessary for the browsers I wanted to support; but you’re right, of course: we could go on and do a lot more!

  • Asif Javed

    Good One, I learned new stuff and logic :) thx alot

  • Tony

    Another amazing tutorial Andrew!

    God Bless!

  • http://www.avadis.net بهینه سازی سایت

    THANKS ANDREW
    ITS USEFULL

  • http://www.codylindley.com cody lindley

    Nice work! Much of what you’ve done here is the bases of my up and coming book and hippo.js lib. I’d like to suggest If anyone is looking for more learning/thinking in this area to read DOM Enlightenment and check out hippo.js.

    DOM Enlightenment: http://domenlightenment.com
    hippo.js: https://github.com/codylindley/hippojs

    • http://andrewburgess.ca Andrew Burgess
      Author

      Thanks Cody! I hope any interested readers will check it out, as I will be!

  • http://blog.pathtosharepoint.com Christophe

    A very helpful tutorial, thanks for sharing!

  • http://www.doitthefairway.com Fairway web design

    Great article, I always use jQuery, just taking it for granted really interesting read

  • http://creative.petalburgwoods.com/ Petalburg Woods

    Thanks for the great tutorial. It’d be great if you guys made an update, though.

  • http://blog.pathtosharepoint.com Christophe

    One (more) comment. I just built a “create” function and was careful to use className for IE7 support, just as explained in your tutorial. But personally I don’t like the idea of modifying the original object passed as argument, as you do here (delete attrs.className;). The attrs object might be reused somewhere else, or simply to create multiple elements.

  • ann

    thanks this post

  • http://downloadsia.com dwys

    this is very hepfull and thanks Andrew!
    thanks for tutorial. . .

    wait the next your totorial. . .

  • Felix D.

    This is probably the most useful tutorial i´ve seen so far on net.tutsplus, but no comments ??
    Whatever… good work !

  • IA Web

    Is there some error in this code
    Dome.prototype.forEach(callback) { this.map(callback); return this;};

    • SRAVAN

      Dome.prototype.forEach = function(callback) { this.map(callback); return this;};

  • han

    good,most useful, thanx

  • SRAVAN

    Thank you for the useful tutorial