Search

Pony Foo

Ramblings of a degenerate coder

Getting Over jQuery

(2 comments)reading time: , published

We've looked at doing some of the things that you can do in native code. So far, we've covered AJAX, event handling, event delegation, DOM querying, and DOM manipulation. If you haven't already read that, you probably should.

Why do we really use jQuery? Sure, it simplifies things. But do we really need all those abstractions? Can't we get away with just a few of the most basic of them? In this article I'll examine the most frequently used portions of its API, and look at the different ways we can rewrite those everyday utilities using plain JavaScript.

jquery.jpg

If we look at their API documentation, we can quickly categorize the features we use most frequently.

  • AJAX
  • Attributes, CSS, .data
  • Effects, Animations
  • Events
  • DOM Querying, Selectors
  • DOM Manipulation
  • Plugins

That certainly looks like a lot. Lets break it down, and attempt to arrive at the same functionality, but without jQuery. Our aim in doing so, isn't just getting rid of jQuery for the sake of doing so, but thinking about why we'd want it in the first place. Furthermore, we will be gaining insight into how jQuery operates, what is needed, what is not, and maybe even more importantly, understanding and becoming capable of performing these operations on our own.

Scope

I previously mentioned the micro library movement, which is awesome, too. Here, though, we will pick a few battles of our own, and have a shot at resolving them without resorting to external dependencies. Other than what browsers provide, that is.

Keep browser support in mind. In each of my solutions, I'll tell you what the browser support is for that particular approach. I will mostly speak about future-proof solutions, but most of what I'll be talking about probably won't work in IE 6. So keep an eye on that.

Even if you are working in a project that must support older browsers, for whatever reason, I think you'll still find value in these excerpts. Maybe they aren't that useful to you today, maybe they are. One thing is certain though, the benefit of learning the underlying browser API won't be going away anytime soon.

AJAX

I wanted to give you an update on AJAX. We've already somewhat covered how to write native requests, but lets take it up a notch.

At this point, I think I should introduce you to XHR2. Lets start by talking about browser support. As you can see, XHR2 support includes anything that's not IE < 10 || Android < 3.0. That's not very encouraging, but it's workable.

The fun in XHR2 comes from being able to set a responseType. Here is a table of possible values, adapted from what can be found on MDN.

Valueresponse data type
'text'String (this is the default, too)
'arraybuffer'ArrayBuffer
'blob'Blob
'document'Document
'json'JSON object

Note that the 'json' value is currently only supported by Firefox and Opera. If you want to fetch JSON data in a cross-browser manner, your best bet is setting responseType = 'text', and then parsing the response like so: JSON.parse(xhr.response).

From the resources listed above, we can gather that Blob is a great representation if we want to fetch images, or any other binary file. 'document' should be used for XML. json of parsed 'text' for JSON, and 'text' for pretty much everything else.

As far as sending data to the server goes, there are a few options. we could stick to using a simple String value.

var xhr = new XMLHttpRequest();
xhr.open('POST', '/api', true);
xhr.onload = function(e){
    if(this.status === 200){
        console.log(this.response);
    }
};
xhr.send('data!');

But we're already used to doing that. What's new is we can send form-like data using FormData.

var formData = new FormData();
formData.append('username', 'carlos');
formData.append('email', 'cslim@geocities.com');
formData.append('dob', 1940);

var xhr = new XMLHttpRequest();
xhr.open('POST', '/register', true);
xhr.onload = function(e){
    if(this.status === 200){
        console.log(this.response);
    }
};
xhr.send(formData);

We don't necessarily have to create the FormData from scratch, either. Suppose we had a form.

<form id='registration' name='registration' action='/register'>
    <input type='text' name='username' value='carlos'>
    <input type='email' name='email' value='cslim@geocities.com'>
    <input type='number' name='dob' value='1940'>
    <input type='submit' onclick='return sendForm(this.form);'>
</form>

Then we could derive our AJAX request data off of it.

function sendForm(form) {
    var formData = new FormData(form);
    formData.append('csrf', 'e69a18d7db1286040586e6da1950128c');

    var xhr = new XMLHttpRequest();
    xhr.open('POST', form.action, true);
    xhr.onload = function(e) {
        // ...
    };
    xhr.send(formData);

    return false; // we're already submitting the form through AJAX.
}

var form = document.querySelector('#registration');
sendForm(form);

Similarly to responses, .send() supports passing Blob data, if we need to perform asynchronous file uploads.

In older browsers, lots of different methods are used to upload files asynchronously. Flash, iframes, anything goes. Other than file uploads or otherwise using form data directly, though, we are just fine doing AJAX in older browsers, as long as we don't pretend to use the XHR2 API. This API mostly improves our asynchronous file upload capabilities, but we are otherwise fine without it.

Attributes, CSS, and .data

Not everything has to be as complicated as AJAX is, and Element attributes are never reason enough to warrant the inclusion of a heavy-weight library such as jQuery.

Lets look at all of these in turn.

.attr(name, val) is just sugar. Once we have an element, presumably obtained using something similar to document.querySelector('main'), we can use .setAttribute(name, val) to set the attribute, or getAttribute(name) to retrieve its value.

.prop(name, val) does pretty much the same thing, except there are some parse hooks in place to return booleans or numbers, rather than always returning strings. Which is nice, but that doesn't justify an enormous footprint either.

When it comes to CSS, it pains me to read the source code of jQuery plugins and find out that they set up tens of different styles directly in their JavaScript code, why not use classes instead? That's what they are for! Unless you are writing CSS that depends on the dynamic calculations you are performing in your JS code, there is no reason not to use a class, instead.

Once that's out of the picture, we're left with two applications for manipulating classes within JS code: logic to hide or display DOM components, and logic to add or remove classes from our nodes.

You should be ashamed to even think of using jQuery for the former. These would be all you need to type to get that working:

// display
element.style.display = 'block';

// hide
element.style.display = 'none';

When it comes to the latter, we can use classList, which doesn't have great support, or we can simply use className. If we find ourselves in need to add or remove classes, then we will have to resort to using regular expressions to figure out how to remove classes from our elements.

!function(exports){
    var class_list = !!document.body.classList;
    var s = '(\\s|^)'; // space or start
    var e = '(\\s|$)'; // space or end

    function getRegex(className){
        return new RegExp(s + className + e, 'g');
    }

    exports.addClass = function(element, className){
        if(class_list){
            element.classList.add(className);
        }else{
            element.className += ' ' + className;
        }
    };

    exports.removeClass = function(element, className){
        if(class_list){
            element.classList.remove(className);
        }else{
            var rclass = getRegex(className);
            element.className = element.className.replace(rclass, '');
        }
    };

    exports.hasClass = function(element, className){
        if(class_list){
            return element.classList.contains(className);
        }else{
            var rclass = getRegex(className);
            return element.className.match(rclass);
        }
    };
}(window);

That wasn't that hard, either. As we are on the subject, let me give you some added value, and talk about getComputedStyle. Supported in every browser except for IE < 9, getComputedStyle returns the resulting value of applying every style on an element. The coolest feature of this method, though, is that it enables us to grab the computed _pseudo-element styles. For example, we could grab the ::after styles on a <blockquote> element.

Here you have an example taken from MDN:

<style>
    h3:after {
        content: ' rocks!';
    }
</style>

<h3>generated content</h3> 

<script>
    var h3 = document.querySelector('h3');
    var result = getComputedStyle(h3, ':after').content;

    // > ' rocks!'
    console.log('the generated content is: ', result);
</script>

Before we move forward, there's one more attribute accessor we might want to talk about. The .data API. Similarly to .prop, it works by probing the value in your data-* attributes, parsing true, false, numbers, and JSON objects, or just returning a String. One important difference here, is that jQuery sets up a cache for these values. This helps prevent querying the DOM time and again for stuff that isn't going to change. Under the assumption that we are manipulating data attributes solely through their API, that is.

Other than that, a simplified data API might look like:

function data(element, name, value){
    if (value === undefined){
        value = element.getAttribute('data-' + name);
        return JSON.parse(value);
    }else{
        element.setAttribute('data-' + name, JSON.stringify(value));
    }
}

Keep in mind you might also want to use the dataset API, but IE < 11 doesn't support it.

If you were to add a little cache to reduce DOM querying, you'd have your own little awesome .data API!

Effects, Animations

In this category, I'll get straight to the point. We'll want to use CSS for any kind of animations. If it's fading effects you are after, then you can resort to transitions, instead.

When it comes to animations, there is one more option, though. We could use setInterval to set up a loop where we animate something, for example, if we want to move an element with absolute positioning all around our viewport.

setInterval(function(){
    // move it a bit
}, delay);

I always had problems with setInterval. Personal problems. You see, the delay you apply as the second argument counts from the moment the function triggers, not the moment the execution ends. As a result, if your function takes 400, and you've set a delay of 600, The calls will eventually overlap so much, making a mess of everything. For that reason, I prefer doing a bit of extra work.

function loop(fn, interval){
    return setTimeout(function(){
        fn(function(){
            loop(fn, interval);
        });
    }, interval);
}

loop(function(done){
    // do our trick

    done(); // continue our loop
}, 600);

The difference is subtle, but now, we can invoke done whenever we are done, so our loop will run sequentially, not in parallel, whichmakes no sense. It's supposed to be an interval, right?

Using requestAnimationFrame

Enough with the setInterval rant. We shouldn't have to use either of these when it comes to animations. A better option is available. Yes, I'm talking about requestAnimationFrame. No, it has pretty bad browser support. Android doesn't support it at all. IE < 10 doesn't care about it either.

requestAnimationFrame allows us to perform a setInterval-like operation just before every repaint. This method takes a callback, our operation, and passes our callback an argument, with a timestamp, so we don't have to make assumptions about the time elapsed.

Here is an usage example, forked from the example on MDN.

!function(w, raf) {
    w[raf] = w[raf] ||
             w.mozRequestAnimationFrame ||
             w.webkitRequestAnimationFrame ||
             w.msRequestAnimationFrame;           
}(window, 'requestAnimationFrame');

var start = Date.now();

function step(timestamp) {
    var progress = timestamp - start;
    d.style.left = Math.min(progress / 10, 200) + 'px';
    if (progress < 2000) {
        requestAnimationFrame(step);
    }
}

requestAnimationFrame(step);

Chris Coyier also provides a few, nice usage examples, on his blog.

Events

A lot has improved in the jQuery event API over time. It used to be all over the place, nowadays we mostly have the .on and .off methods, and those handle pretty much everything we need.

So what are the strong points for jQuery in event handling? Well, they make it really easy to perform event delegation.

$('ul').on('click', 'li', function(){
    console.log('li clicked!', this);
});

This seemingly innocent handler will be triggered whenever we click on any <li>, yet the event handler will be on the parent <ul>. The way this works is that whenever an <li> is clicked, the event will bubble up to the <ul>. The <ul> will have an special handler, provided by jQuery, which will trigger our handler .applying the <li> as this.

If you are just realizing how complicated this is to grasp, that's probably because how powerful the abstraction is. The implications of this might not be obvious at a glance, but the end result is that you get a much more performant experience. Rather than setting up an event handler for each <li>, which could potentially be thousands, you are setting a single <ul> event listener instead.

Other than event delegation, their API is once again really easy to implement by hand, and you might want to check out my previous post on the subject to wrap your head around that.

If you want to try going native, a suggested approach consists of barely two lines of code.

var $ = document.querySelectorAll.bind(document);
Element.prototype.on = Element.prototype.addEventListener;

Once our ridiculously small library is in place, we can attach event handlers using our new API.

$('#featured')[0].on('keyup', handleKeyUp);

Concise enough.

DOM Querying, Selectors

One of the most important mechanisms in browsers is querying the DOM to obtain a reference to HTML nodes. Yet, querySelector, by far the best option to perform such requests, is relatively unknown to the average developer. It's as if they're stuck with either getElementById, or using jQuery.

Truth is, querySelector and querySelectorAll are broadly supported in all major browsers, with the exception of IE < 8. That is really good browser support. That is, in fact, one of the major reasons jQuery decided to drop support for IE < 9 in their v2 branch.

With querySelector being implemented across all browsers, the novelty in jQuery is reduced to the ability to extend the selector engine by adding your own, custom selectors. This just adds to the confusion and isn't really necessary. I'd recommend staying away from that.

DOM Manipulation

There isn't a lot left to cover about DOM manipulation that wasn't covered in the other topics we've been discussing. If we look at the API documentation once again, you'll notice we've accounted for most of the methods in the category. The ones we didn't mention are mostly measure computations, DOM altering methods, or methods such as .val(), .text() and .html(), which don't really abstract any cross-browser limitations away.

When it comes to altering the DOM, the native methods can be found on MDN. Once we know about those, all jQuery really does is build on top of the Node API, providing us with some syntactic sugar, such as insertAfter does.

Plugins

plugins.jpg

Ah, plugins! Do we really need everything to be a jQuery plugin? I get ecstatic whenever I find a small library, which performs its intended objectives really well, has a succint API, and doesn't freaking depend on jQuery for absolutely no reason.

I guess my point is, make it a conscious decision. Don't mindlessly turn your ten line miracle worker into a jQuery plugin just because you want to use .hide() and .show(). Write native code instead. You'll probably learn to write better code while at it, and more people will be able to use it, to boot.

Oh, and stay the hell away from jQuery UI, too. Thank you.

Unless you are really using it extensively. If you only need the dialogs, you can get away with just a few lines of CSS code!

Need a Talk?

Below is an excellent talk on jQuery, by Remy Sharp. He addresses a lot of important points, and raises some very good questions. He also presents a minimal library called min.js, which I think shows a lot of promise. In this half hour ish talk, you'll learn how you can actually write native BOM pretty effortlessly, without having to resort to a jQuery-like library.

remy-on-jquery

In Conclusion

I don't expect you to shelf jQuery right away. I'm just attempting to enlighten you, there is another way to do things. jQuery is great and all, but it's been around for almost ten years, so it's understandable that it lost some value along the way. It is good if you are actually using many of its features, but you should ponder about whether this is a fact for you, or if you are simply using it because, hey, it's already there.

And it's not jQuery's fault, but rather, we should be complimenting the browsers for this change. Going forward, IE11 is finally putting an end to all the non-sense set forth by it's predecessors. They're really trying hard this time to set it apart from "old IE" distributions.

Now that all major browsers offer automatic updates, jQuery will steadily decline in value. The ultimate purpose of the library, dealing with the multitude of cross browser issues present in older browsers, is subsiding. In its current state, jQuery will eventually become a library that just provides a somewhat nicer API than native browser JavaScript does.

If you think there is a topic I didn't uncover, please let me know, and I'll consider it for a future blog post.

Happy experimenting!

Comments(2)

Callum Macrae

I always had problems with setTimeout, personal problems.

I'm pretty sure you meant setInterval there!

Another advantage of .on is that if you're going to be adding or modifying elements below the ul, you won't have to worry about adding more event listeners to them (in fact, that's sometimes the main advantage). That can also be a disadvantage, though.


I'm starting to see more people moving away from jQuery. At the point I started writing my book (then JavaScript for jQuery Coders), there were a pretty large number of people – usually backend developers – who would know a bit of jQuery, and would use it in their project without any real knowledge of JavaScript. However, with the improvements of both JavaScript and a certain browser, writing vanilla JavaScript has become easier, and so the number of people using only jQuery has dropped.

Great article! I still wouldn't have usually considered not using a library for animations, but now I realise that it actually isn't as tricky as I thought it was.

Nicolas Bevacqua

Nice catch, I'll fix that. Thanks!

You are absolutely right about event delegation, you have to know where and how to use it, and to do so you must understand what it is that you are doing.

I have a similar feeling about it, I think we are entering an era where JavaScript is much more ubiquitous. We now have JavaScript in the server, in the database, and JSON has been dominating the REST API scene for a while now. So being "the back-end guy" is even more of a lame excuse than it used to be.

Glad you enjoyed it :D

Pony
Foo
Pony
Foo
Pony
Foo