Advertisement
  1. Code
  2. Web Development

Build Your First JavaScript Library

Scroll to top
Read Time: 13 min

Ever marveled at the magic of React? 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 jQuery.

We use JavaScript libraries nearly every day. Whether it's for implementing an algorithm, providing an abstraction over an API, or manipulating the DOM, libraries perform many functions in most modern websites.

In this tutorial, we’re going to take a (decidedly shallow) stab at building one of these libraries from scratch. We will work on creating a library for DOM manipulation, like jQuery. 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 Chrome, Firefox, and Safari, but might not work on older browsers like IE.
  • 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.

1. Creating the Library Boilerplate

We’ll start with the module itself. We will use ECMAScript Modules (ESM), a modern way of importing and exporting code on the web.

1
export class Dome {
2
    constructor(selector) {
3
    
4
    }
5
}

As you can see, we are exporting a class called Dome. This will be the main part of the library and will represent an element or an array of elements.

2. Getting Elements and Creating Dome Instances

The Dome constructor 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.

1
constructor(selector) {
2
    let els;
3
    if (typeof selector === "string") {
4
    	els = document.querySelectorAll(selector);
5
    } else if (selector.length) {
6
    	els = selector;
7
    } else {
8
    	els = [selector];
9
    }
10
    this.elements = els
11
    this.length = els.length;
12
}

We’re using document.querySelectorAll to simplify the finding of elements. 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. Then, we set this.elements to the elements and this.length to the number of the elements.

3. 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:

1
map(callback) { // put this inside the Dome class as a method
2
    let results = [];
3
	for (let i = 0; i < this.length; i++) {
4
		results.push(callback.call(this, this.elements[i], i));
5
	}
6
	return results;
7
}

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:

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

By doing it this way, we ensure that 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 very simple:

1
forEach(callback) {
2
	return this.elements.forEach(callback)
3
}

Since NodeLists and Arrays come with the forEach method by default, we can simply forward the call to this.elements.

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

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 nodes (new Dome("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!

1
mapOne(callback) {
2
	const m = this.map(callback);
3
	return m.length > 1 ? m : m[0];
4
};

4. Working With Text and HTML

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

1
text(text) {
2
	if (typeof text !== "undefined") {
3
		return this.forEach(function (el) {
4
		el.innerText = text;
5
	});
6
	} else {
7
	    	return this.mapOne(function (el) {
8
			return el.innerText;
9
		});
10
	}
11
}

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 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.

1
html(html) {
2
	if (typeof html !== "undefined") {
3
		this.forEach(function (el) {
4
			el.innerHTML = html;
5
		});
6
		return this;
7
	} else {
8
		return this.mapOne(function (el) {
9
			return el.innerHTML;
10
		});
11
	}
12
}

Like I said: almost identical.


5. Manipulating 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. Essentially, we are just using the classList.add method on each element. When a string is passed, only that class is added, and when an array is passed, we iterate through the array and add all of the classes contained.

1
addClass(classes) {
2
	return this.forEach(function (el) {
3
		if (typeof classes !== "string") {
4
			for (const elClass of classes) {
5
				el.classList.add(elClass);
6
			}
7
		} else {
8
			el.classList.add(classes);
9
		}
10
	});
11
}

Pretty straightforward, eh?

Now, what about removing classes? To do this, you do almost the same thing, just with classList.remove.


6. 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.

1
attr(attr, val) {
2
    if (typeof val !== "undefined") {
3
    	return this.forEach(function (el) {
4
    		el.setAttribute(attr, val);
5
    	});
6
    } else {
7
    	return this.mapOne(function (el) {
8
        	return el.getAttribute(attr);
9
    	});
10
    }
11
}

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.

7. Creating Elements

We should be able to create new elements, as any good library can. Of course, this would be no good as a method on a Dome instance, so let’s create it outside of the Dome class.

1
export function create(tagName,attrs) {
2
3
}

As you can see, we’ll take two parameters: the name of the element and an object of attributes. Most of the attributes will 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:

1
export function create(tagName, attrs) {
2
    let el = new Dome([document.createElement(tagName)]);
3
	if (attrs) {
4
		for (let key in attrs) {
5
			if (attrs.hasOwnProperty(key)) {
6
				el.attr(key, attrs[key]);
7
			}
8
		}
9
	}
10
	return el;
11
}

As you can see, we create the element and send it right into a new Dome object. Then, we deal with the attributes. 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?

8. Appending and Prepending Elements

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

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

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
I’m using “new” to mean elements not yet in the DOM; existing elements are already in the DOM.

Let’s step through it now:

1
append(els) {
2
3
}

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:

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

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:

1
parEl.appendChild(childEl);

So, altogether, this is what we have:

1
append(els) {
2
	return this.forEach(function (parEl, i) {
3
		els.forEach(function (childEl) {
4
			if (i > 0) {
5
				childEl = childEl.cloneNode(true);
6
			}
7
			parEl.appendChild(childEl);
8
		});
9
	});
10
}

The prepend Method

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

1
prepend(els) {
2
	return this.forEach(function (parEl, i) {
3
		for (var j = els.length - 1; j > -1; j--) {
4
			childEl = i > 0 ? els[j].cloneNode(true) : els[j];
5
			parEl.insertBefore(childEl, parEl.firstChild);
6
		}
7
	});
8
}

The difference 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.

9. Removing Nodes

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

1
remove() {
2
	return this.forEach(function (el) {
3
		return el.parentNode.removeChild(el);
4
	});
5
}

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?

10. Working With Events

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

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

1
on(evt, fn) {
2
	return this.forEach(function (el) {
3
		el.addEventListener(evt, fn, false);
4
	});
5
}

This is simple enough. We just loop through the elements and use addEventListener on each.

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

1
off(evt, fn) {
2
	return this.forEach(function (el) {
3
		el.removeEventListener(evt, fn, false);
4
	});
5
}

11. Using the Library

To use Dome, simply put it in a script and import it.

1
import {Dome, create} from "./dome.js"

From there, you can use it like this:

1
new Dome("li")
2
...

Make sure that the script you are importing it in is an ES Module.

That’s It!

I hope you give our little library a try, and maybe even extend it a bit. As I mentioned earlier, I have it up on GitHub. 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:

This post has been updated with contributions from Jacob Jackson. Jacob is a web developer, technical writer, freelancer, and open-source contributor.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.