My little dice-rolling app, RPG Dice Bag, isn’t a severely complicated app, so I thought I’d take a chance at removing the jQuery JavaScript Library and rewriting the code to use “vanilla” JavaScript. I, like probably a lot of other web-developers, have become increasingly dependent on jQuery to handle the heavy (and light) lifting in our JavaScript-powered websites and apps so this would be a great challenge for me to learn how to do it the old-fashioned way. Also, no AJAX calls necessary, so that’s a plus.
Removing jQuery from a project is pretty simple, you remove the jquery.js file (jquery.min.js if you’re in production) and you’re all set, but I soon realised that after nearly 3 years of habitually including it to every web-project I came near, I’ve plum forgotten (or never learned) how to do many fundamental things.
document.ready
The first challenge that hit me was how to listen for when the document was ready for JavaScript events to be attached to the DOM. In jQuery, it’s rather straightforward and simple.
$(document).ready(function() {
// all ready for you
});
There is no such thing as “document.onready” in bare-bones JavaScript, but searching for it lead me down a path of learning about the native way to handle events, known as addEventListener, which can be applied to all DOM elements and is also used to handle clicks (coming up later). It’s supported by most modern browsers (I’ve completely cut IE out of my diet), so it became my first step to removing the jQuery code from my app. There are various ways of checking whether the DOM is ready, but I opted for the following:
document.addEventListener('readystatechange', function() {
if (document.readyState === 'complete') {
// let's do this thing!
}
});
CSS-Style Selectors
One of the greatest benefits of jQuery is to be able to drill down into the DOM by using CSS-style selector strings to specify which element you’re looking for. Something as complicated as “#app ul .title a:hover > span.left" can be used to choose one specific element, or a set of elements that are situated in various places around the document. Unfortunately there isn’t a direct replacement in native JavaScript, but a combination of getElementById, getElementsByTagName and getElementsByClassName, as well as iterating through the results, can be used to select the element(s) you need and not being able to rely on CSS pseudo-classes or some of the more esoteric selectors may result in structuring the HTML in a less convoluted way, avoiding unnecessary strain on the browser who has to deal with all your crazy shenanigans.
// Get the data-foo attribute of all links inside a .button list
var buttons = document.getElementsByClassName('buttons'),
foo = '';
for (var i = 0; i < buttons.length; i++) {
var links = buttons[i].getElementsByTagName('a');
for (var j = 0; j < links.length; j++) {
var link = links[j];
foo += link.attributes['data-foo'];
}
}
Edit: My good friend Torjus Bjåen made me aware of the querySelectorAll function, which provides the same CSS-style selector capabilities that jQuery does, albeit without some of the more “magical” selectors. It works like you’d expect and can be used to easily fetch elements that may be spread about the DOM.
var links = document.querySelectorAll('.buttons a.special');
for (var i = 0; i < links.length; i++) {
links[i].style.backgroundColor = '#f09';
}
Click Event Delegation
To avoid overloading the browser with too many event handlers in the cases where you have sets of elements that need to react similarly, you should try to add your events to their parent element and delegate the event to the correct element by using a selector.
$('#buttons').on('click', 'a', function(event) {
// button was clicked
});
Translating this to vanilla JavaScript isn’t really possible without understanding that the Event object which is passed to the function you attach to an event has a property called target which is the DOM element which received the event. Note that this is not necessarily the element which you attached the event listener to, but it could be the child of the child of the child of the child of said element, which may be further down in the tree than you really need to go. In that case you’ll need to traverse your way upwards, using the parent property until you find the one you’re looking for (if you ever do).
var elem = document.getElementById('buttons');
elem.addEventListener('click', function(event) {
var target = event.target;
while (target.tagName !== 'A') {
target = target.parent;
}
if (typeof target !== 'undefined') {
// "#buttons a" was clicked
}
});
Auto-wrap Everything in Conditionals
All this work was getting me closer to understanding the fundamentals of the DOM and how JavaScript (and the browser) worked when trying to parse and understand the ever-complex world of HTML. However, there is one small thing that jQuery does which I’m going to miss, nevermind how much “better” my code is now.
// jQuery
$('#buttons').css('background-color', '#f09');
// native
var buttons = document.getElementById('buttons');
buttons.style.backgroundColor = '#f09';
The biggest difference between those two examples (which accomplish the same thing) is that jQuery will fail silently if the element doesn’t exist. This is a major difference for web development where your dynamic HTML may not always be in sync with the JavaScript you wrote for it and when JavaScript hits an error it stops, spits out something in the console and then goes home to sulk. jQuery essentially removes the potential headache of an app-breaking bug resulting from not wrapping the line in an if statement, which is a blessing for any project of sizeable complexity.
–
Becoming too dependent on jQuery is one of the many casualties of the splintered JavaScript support that the various browsers support and even my tiny app probably has some code that isn’t supported in any of the browsers if you go far enough back. Its biggest strength (and most often repeated party line) lies in the cross-browser support, but my own experiences shows that there are a slew of jQuery-isms that I’m learning in lieu of doing it “the right way”, which will probably just end up hurting me in the long run, especially when I see how complicated things can get when I have to traverse the DOM tree manually.
I’m not saying that using jQuery is bad (hell, I’d rather avoid XMLHttpRequest for the rest of my life if I can help it), but it’s interesting to note how much of the DOM is hidden behind its flair and rounded corners and the dangers that are posed by relying on dangerously slow and memory intensive CSS-style element selection for something that could be handled faster/harder/stronger with a slight restructuring of your HTML.