Building a DOM Library Inspired by jQuery

Building a DOM Library Inspired by jQuery

In this tutorial we’re going to build a jQuery-esque library from scratch. We’ll be targeting the latest browsers (IE9+) so we can make use of the some of the features that ECMAScript 5 provides out of the box.

How to Build a DOM Library

Collections & functions

In JavaScript we can store data in arrays and objects, and combinations of those. A common data structure is a collection. A collection is an array of objects, for example:

var users = [
  {username: 'jonny89', age: 25, active: 1},
  {username: 'mikeyfan', age: 42, active: 0},
  {username: 'rogerp', age: 13, active: 1}
];

JavaScript has a few built-in methods to loop arrays that are very useful for collections:

// Filter collections by any criteria
var adults = users.filter(function(user) {
  return user.age > 18;
});
//^[{username:'jonny89', age:25, active:1},
//  {username:'mikeyfan', age:42, active:0}]

var active = users.filter(function(user) {
  return user.active;
});
//^[{username:'jonny89', age:25, active:1},
//  {username:'rogerp', age:13, active:1}]

// Extract info from the collection
var usernames = users.map(function(user) {
  return user.username;
});
//^['jonny89','mikeyfan','rogerp']

var ages = users.map(function(user) {
  return user.age;
});
//^[25,42,13]

Since these operations are so common, we can create higher-order helper functions to work with collections. A higher-order function is a function that takes other functions as input, or returns a function as output, for example the dot helper below lets you extract a particular property from each item in the collection; we can use it with map to get the results as an array:

// Higher-order helper function
function dot(s) {
  return function(x) { //<-- returns a function
    return x[s];
  };
};

var usernames = users.map(dot('username'));
//^['jonny89','mikeyfan','rogerp']

Functional programming lets us work at a very high level of abstraction because functions are objects with methods and properties that can be passed around just like any other object.

The next helper we need work with functions is compose. Composition of functions lets us create new functions from other functions and execute them in a sequence; it makes nested calls nicer. Composition is usually implemented right to left, but we’re going to implement it left to right so we can read it in the order we’d expect:

function compose(/*fns...*/) {
  return [].reduce.call(arguments, function(f,g){
    return function() {
      return g(f.apply(this, arguments));
    };
  });
}

function add1(x){ return x + 1 }
function by2(x){ return x * 2 }
function square(x){ return x * x }

// Nested
var result = add1(by2(square(2))); //=> 9

// Composition
var comp = compose(square, by2, add1);
var result = comp(2); //=> 9

Querying the DOM

The DOM API is known to be inconsistent, especially in old browsers, but we’re not targeting those. In modern browsers all we really need is querySelectorAll. DOM methods return pseudo-arrays of elements, not real arrays; they are objects similar to the arguments object, in that they can be looped and accessed but they don’t have all the useful methods that arrays have. We need a simple helper to convert those objects to real arrays so we can treat them as collections:

function toArray(x) {
  // Not an array-like object
  if (!x.length || typeof x != 'object') {
    return x;
  }
  return Array.prototype.slice.call(x);
}

Now we can query the DOM comfortably:

function query(sel, el) {
  return toArray((el||document).querySelectorAll(sel));
}

// Querying the document
var els = query('ul li');

// Querying another element
var els = query('p', document.body);

Building the library

Let’s begin by wrapping our code in an IIFE (Immediately Invoked Function Expression), to avoid leaking variables to the global scope, then assigning the result to window.$ so we can use our library just like jQuery:

window.$ = (function(){
  // code here
}());

The constructor function

First we need a constructor to create new instances of our super powered DOM object, lets call it Dom. The constructor will take one argument, a selector or an element. jQuery’s constructor function is huge as it deals with elements, selectors, arrays, HTML, etc. but we’re trying to keep it simple. Then we’ll setup the length and cache the first collection, as we’ll be using it later:

function Dom(sel) {
  this.el = typeof sel == 'string'
    // a selector
    ? query(sel)
    // an element or pseudo array of elements
    : [].concat(toArray(sel));
  this.first = this.el;
  this.length = this.el.length;
}

Next, let’s create a shortcut for our constructor so we can call it without using the new keyword. We return this function from the IIFE to make it globally accessible:

window.$ = (function(){
  function Dom(sel){
    ...
  }
  // Call constructor without `new`
  function $(sel) {
    return new Dom(sel);
  }
  return $; // make it global
}());

Now we can use $ from the outside to query the DOM.

When we log this to the console we will get a Dom object, from our constructor:

var list = $('li');
//^ Dom {el: Array[3], first: Array[3], length: 3}

We can access the elements array with list.el or a single element by index using list.el[n].

A strong foundation

Let’s simplify the way we get elements out of the collection. We’re going to create a public get method that will return the array of elements or a single element by index:

Dom.prototype = {
  // Get element(s)
  get: function(idx) {
    return idx == null ? this.el : this.el[idx];
  }
};

Now we can get items like so:

list.get(); //=> [li,li,li]
list.get(1); //=> li

One of the simplest DOM methods to implement is parent. The parent method returns a collection with the immediate parent of each element on the previous collection. We can use the native map with our dot helper to extract the parentNode of each element:

parent: function() {
  // Modify previous collection
  this.el = this.el.map(dot('parentNode'));
  // Adjust length
  this.length = this.el.length;
  return this; // chain
}

This works, but there is one problem: if two or more elements have the same parent then we would have duplicate elements in our new collection. For example:

$('li').parent().get(); //=> [ul,ul,ul]

We only need unique elements:

function unique(xs) {
  return xs.filter(function(x, idx) {
    return xs.indexOf(x) == idx;
  });
}

You can find other similar helpers on StackOverflow.

With our unique helper we can update the parent method to filter out duplicates:

parent: function() {
  // Modify previous collection
  this.el = unique(this.el.map(dot('parentNode')));

With this technique we could implement other similar methods that extract a single property from each element in the collection, like next and prev:

this.el.map(dot('nextElementSibling'))
this.el.map(dot('previousElementSibling'))

But there’s a pattern that has to be repeated many times: assigning the new collection, updating the length, and returning the instance for chaining. We can abstract these steps into a method called _update. We prefix it with an underscore because it’s meant for internal use:

_update: function(result) {
  this.el = [].concat(result); // make sure it's an array
  this.length = this.el.length;
  return this;
}

Then we need to update the parent method:

parent: function() {
  return this._update(unique(this.el.map(dot('parentNode'))));
}

This works, but it looks a bit funky, and we’d still have to repeat unique(this.el.map(...)) for every method. Let’s abstract this further by creating our own map method which will take a function and apply it to each element in the collection, then filter out the duplicates and return an updated collection:

map: function(f) {
  var result = unique(this.el.map(f));
  return this._update(result);
}

Now our parent method will look more concise:

parent: function() {
  return this.map(dot('parentNode'));
}

The next and prev methods can be implemented very easily with map:

next: function() {
  return this.map(dot('nextElementSibling'));
},
prev: function() {
  return this.map(dot('previousElementSibling'));
}

Implementing children requires a bit more work. Let’s try implementing it as we did with the other methods:

children: function() {
  return this.map(dot('children'));
}

But when we run this method on a collection we get unexpected results:

$('ul, div').children();
//^ [HTMLCollection[3], HTMLCollection[1]]

What happens is that children returns a pseudo-array but we need a real array. We need to modify our map method so it makes sure that whatever gets mapped is an array. Naively we could simply pass a callback and call toArray inside on each element after applying the function, for example:

map: function(f) {
  var result = unique(this.el.map(function(){
    return toArray(f.apply(this, arguments));
  }));
  return this._update(result);
}

But this is definitely not pretty. In fact, we already have a helper to get around this ugly syntax, remember compose?

map: function(f) {
  var result = unique(this.el.map(compose(f, toArray)));
  return this._update(result);
}

Good, now we get arrays:

$('ul, div').children(); //=> [Array[3], Array[1]]

This is not exactly what we want though. What we really need is a single array of elements, not a nested array. We must make sure the resulting collection is always flattened. The flatten helper solves this issue:

// Simple one level flatten
function flatten(xs) {
  return Array.prototype.concat.apply([],xs);
}

Let’s update the map method to flatten the collection, and break it into two steps for readability this time:

map: function(f) {
  var result = this.el.map(compose(f, toArray));
  result = unique(flatten(result));
  return this._update(result);
}

As you can see now we get a single array of elements:

$('ul, div').children(); //=> [li, li, li, p]

Adding complexity

Now that we have a strong foundation we can start implementing more complicated DOM methods.

Let’s begin with parents. The parents method returns a new collection with all the parents of each element in the previous collection. The difference between parent and parents is that the former only grabs the first parent, while the later grabs all parents in the hierarchy. In jQuery most methods also accept a selector to filter the collection further. We’ll see a possible implementation in the next section but for now lets keep it simple.

We can no longer use the dot helper to implement parents as we need to loop for as long as there are parents in the hierarchy. The implementation is slightly more complicated:

parents: function() {
  return this.map(function(x) {
    var result = [];
    while (x = x.parentNode) {
      result.push(x);
    }
    return result;
  });
}

Again, this looks ok, but it seems like we’d have to repeat this pattern to implement methods like nextAll or prevAll, which do the same thing with a different property. We want to abstract this into a higher-order function just as we did with dot at the beginning. Lets call our helper loop, as that’s what we’re doing:

function loop(s) {
  return function(x) {
    var result = [];
    while (x = x[s]) {
      result.push(x);
    }
    return result;
  };
}

Now we can update the parents method to one pretty line:

parents: function() {
  return this.map(loop('parentNode'));
}

The nextAll and prevAll methods look as you’d imagine:

nextAll: function() {
  return this.map(loop('nextElementSibling'));
},
prevAll: function() {
  return this.map(loop('previousElementSibling'));
}

Let’s think about siblings. We want to grab all the previous siblings and all the next siblings for each element in the collection. Again, we cannot simply use dot or loop as we’re looping two different properties of a single element. A premature implementation would be to simply use the methods that we already have and concatenate the results, for example:

siblings: function() {
  var prev = $(this.el).prevAll();
  var next = $(this.el).nextAll();
  return this._update(prev.el.concat(next.el));
}

This works, but we can do better. Instead of creating new instances we need to think at the level of collections. What we really want is to join the results of two operations, looping the previous elements and looping the next elements. We already have a loop helper to do this, but we need a helper to join two of these results. The join helper is a higher-order function that will take two functions (operations) and join the outputs into an array:

function join(f,g) {
  return function() {
    var as = arguments;
    return f.apply(this, as).concat(g.apply(this, as));
  };
}

Now we can update siblings to avoid creating new instances, thus improving performance:

siblings: function() {
  var prev = loop('previousElementSibling');
  var next = loop('nextElementSibling');
  return this.map(join(prev, next));

Other useful methods

Our library is already very capable of doing all sorts of DOM operations but still can’t filter collections using a selector like in jQuery. Instead of updating each and every function to take an extra argument and checking the type of it, we can add a new method filter that can be chained after any operation to get a new filtered collection.

Modern browsers provide some support for filtering elements via matches or matchesSelector, but they are in most cases prefixed, and lead to ugly code and usage of polyfills. A common workaround is to use querySelectorAll on the parent element of each element in the collection and check if the elements queried by the selector match the current element:

filter: function(sel) {
  var result = this.el.filter(function(x) {
    return query(sel, x.parentNode).indexOf(x) > -1;
  });
  return this._update(flatten(result));
}

This implementation is simple enough for most cases, but you can try matchesSelector to improve performance if needed.

With filter we can now do this:

$('li').children().filter('a').get(); //=> [a,a,a]

Let’s implement two other useful jQuery functions, end and eq. The end method returns the first collection in the chain, this is why we cached this.first in the constructor. The eq method returns a new collection out of a single element in the previous collection:

end: function() {
  return this._update(this.first);
},
eq: function(idx) {
  return this._update(this.el[idx]);
}

Other methods that grab a single property from an element can be implemented using dot as we know. That includes text, html, val, and even attr can be implemented this way.

Notes

We deal with elements only, not text nodes. This limitation isn’t much of a problem if you have control over your HTML.

You can use map on any collection and pass your own callback:

$('li').map(function(element) {
  var result = [];
  // ...
  return result;
});

Conclusion

Querying the DOM with jQuery might seem like magic but it all comes down to working with collections as you’ve learned. Of course jQuery does much more than this, it does AJAX, promises, animations and provides many useful helpers. But if you just want to query the DOM and know what you’re doing as well as being able to extend its core easily, then you might not need jQuery after all.

Share your ideas and comments below.

Deals

Iconfinder Coupon Code and Review

Iconfinder offers over 1.5 million beautiful icons for creative professionals to use in websites, apps, and printed publications. Whatever your project, you’re sure to find an icon or icon…

WP Engine Coupon

Considered by many to be the best managed hosting for WordPress out there, WP Engine offers superior technology and customer support in order to keep your WordPress sites secure…

InMotion Hosting Coupon Code

InMotion Hosting has been a top rated CNET hosting company for over 14 years so you know you’ll be getting good service and won’t be risking your hosting company…

SiteGround Coupon: 60% OFF

SiteGround offers a number of hosting solutions and services for including shared hosting, cloud hosting, dedicated servers, reseller hosting, enterprise hosting, and WordPress and Joomla specific hosting.