A Year Without jQuery

Dropping the trusted workhorse from our front-end back in 2014 has led to a faster, leaner platform

I joined We Are Colony back in Summer 2014. Six months into the job, we came to a point in our product development requiring the addition of several large features, and the rethinking of some key pieces of our platform design.

Faced by the decision of hacking on top of the code I had inherited when I started, or starting from scratch, I took the decision of going for the latter, which presented the opportunity to make some big changes to the front-end stack and its dependencies - one of which was to drop jQuery, which we did in late 2014.

While I had already completed a few smaller projects using only “vanilla” JavaScript, this was the first large-scale UI-heavy application I had considered building without jQuery. As someone who cut my teeth on jQuery and authored numerous plugins for the ubiquitous library, I’d now come to a point (like many other developers I’d talked to) where I felt a little bit guilty every time I called the fabled $() function. For a long time, I had already been looking for opportunities to use vanilla JS over jQuery wherever I could do so safely in all browsers. I now felt that the time had come both in my personal development, and also in the landscape of the wider front-end world, to say goodbye to my old friend altogether.

18 months later, the lessons I’ve learned from the process of building a UI without jQuery have been extremely valuable, and I hope to share a few of them in this article. What prompted me to write this however, was actually a talk I attended recently at front-end London, entitled How not to use jQuery. While it was a great and informative talk, it highlighted a misconception that I’ve come across recently from several people - that ES6 will save us from jQuery (right after it cures cancer and ends world poverty). I also remember talking to a developer friend recently who told me that his team were looking forward to moving away from jQuery "just as soon as ES6 is more widely supported".

It highlighted a misconception that I've come across recently ... that ES6 will save us from jQuery

I don’t completely understand where this idea comes from, and hopefully it’s not widespread, but I thought it worth addressing anyway. In my mind, ES6 is for the most part a much-needed syntactical progression of the JavaScript language and jQuery is for the most part, a DOM manipulation library with a beautifully designed API. These two things actually have very little in common so I wanted to write this article primarily to prove that you can stop using jQuery today - and you don’t need ES6 or Babel to do it.

So why drop jQuery at all you may ask? Firstly, application overhead and load time (especially on slower devices and connections); secondly UI performance and responsiveness (again particularly on slower devices); and lastly, the removal of unnecessary abstraction — giving you the opportunity to better understand the DOM, the browser and its APIs.

If there was one thing holding all of us back from dropping jQuery it was arguably having to support IE8, but I hope we can agree those days are now soundly behind us (and you have my sympathies if that’s not the case). Missing from IE8 were the browser DOM APIs which could have saved us from jQuery; things such as Element.querySelectorAll(), Element.matches(), Element.nextElementSibling, and Element.addEventListener() — things which now exist consistently across all browsers.

While IE9 and up (including the latest version of Edge) still have problems, they are more or less consistent in terms of what I would consider the “essential” DOM APIs (with the exception of Element.classList in IE9 unfortunately) needed to write a UI-heavy application without jQuery and without the overhead of numerous polyfills and libraries.

There’s no denying however, that jQuery also comes packed with a bunch of useful utility functions, as well as tools for things like Ajax and animation, which is where things get interesting in terms of deciding what and what not to to include in your front-end toolkit.

Helper Functions

I found that as far as utility and helper functions, dropping jQuery provided a great opportunity to write a few helper functions of my own and learn a bit more about browsers and the DOM in the process, which was in some ways the most valuable part of the process for me. This static class of helper methods (which i called “h”) covered basic things like querying child or parent elements, extending objects, and even Ajax, as well as lots of things unrelated to the DOM.

This might sound like attempting to rewrite jQuery, but that was absolutely not the goal. This small collection of convenience helper methods equates to just a tiny fraction of jQuery’s overall functionality, without wrapping Elements or providing any additional abstraction. The native browser APIs mentioned above are what really enable us to interact with the DOM without jQuery, with these functions filling a few small gaps that existed when I embarked on the project.

Following are a few of those helper functions which I found myself needing, and which I also found to be interesting and educational to write. I’m not including these just so that anyone reading can copy and paste them in their project - you may not even have a need for them - but rather to illustrate how easily we can arrive at solutions to common DOM traversal problems, with the above APIs at our disposal.

.children()

/**
 * @param   {Element}     el
 * @param   {string}      selector
 * @return  {Element[]}
 */

h.children = function(el, selector) {
    var selectors      = null,
        children       = null,
        childSelectors = [],
        tempId         = '';

    selectors = selector.split(',');

    if (!el.id) {
        tempId = '_temp_';

        el.id = tempId;
    }

    while (selectors.length) {
        childSelectors.push('#' + el.id + '>' + selectors.pop());
    }

    children = document.querySelectorAll(childSelectors.join(', '));

    if (tempId) {
        el.removeAttribute('id');
    }

    return children;
};

Returns all child elements of a given element which match a provided selector

.closestParent()

/**
 * @param   {Element}       el
 * @param   {string}        selector
 * @param   {boolean}       [includeSelf]
 * @return  {Element|null}
 */

h.closestParent = function(el, selector, includeSelf) {
    var parent = el.parentNode;

    if (includeSelf && el.matches(selector)) {
        return el;
    }

    while (parent && parent !== document.body) {
        if (parent.matches && parent.matches(selector)) {
            return parent;
        } else if (parent.parentNode) {
            parent = parent.parentNode;
        } else {
            return null;
        }
    }

    return null;
};

Returns the closest parent element to a given element matching the provided selector, optionally including the element itself

.index()

/**
 * @param   {Element}   el
 * @param   {string}    [selector]
 * @return  {number}
 */

h.index = function(el, selector) {
    var i = 0;

    while ((el = el.previousElementSibling) !== null) {
        if (!selector || el.matches(selector)) {
            ++i;
        }
    }

    return i;
};

Returns the index of a given element in relation to its siblings, optionally restricting siblings to those matching a provided selector

Since writing these in 2014, I’ve sinced learned that h.closestParent() now has a native equivalent in the form of Element.closest(), and h.children() in the form of ':scope' psuedo-class allowing us to reference the element itself in queries (e.g. .querySelectorAll(':scope > .child'). While both of these features are fairly new and not universally supported yet, it’s exciting to see how fast browser APIs are catching up (often following jQuery’s influence), and I’m excited to refactor both of these helpers out of our application very soon.

It’s worth noting that one function I’ve omitted from this list due to its length and complexity is h.extend() which I use frequently to extend, merge and clone objects (analogous to jQuery’s $.extend). We don’t use any additional utility libraries such as Underscore or Lodash, so a home-baked extend helper was critical for our application. There are numerous Stack Overflow posts explaining how to achieve such functionality, but I still find myself tinkering with this function regularly as features are added with increasingly complex needs (e.g. the copying of getters and setters, and deep cloning of arrays).

Over the last few years, one resource I’ve always found particularly helpful in my efforts to use vanilla JavaScript is the excellent You Might Not Need jQuery.

Read more @ We Are Colony Blog