In web development, as in life, sometimes we develop patterns in how we think about a topic or achieve a common task. This is necessary, as to do otherwise would waste a lot of mental cycles on trivial problems we’ve already solved. However, these patterns can be hard to break, even when perhaps the pattern is no longer the optimal solution.
One of the most common tasks a front-end developer may handle is traversing the DOM (Document Object Model). Usually this involves locating a DOM element and then manipulating it and/or its contents. Most of us have been using jQuery for this task for the past seven or eight years, as it is one of the core things jQuery has handled well since day one.
In “Internet time” though, seven years is forever. Back in 2006, when jQuery was initially released, Flash was hot and and Rich Internet Applications built with Flex or Silverlight or even Laszlo seemed like they might be the future of the web. Who needs the DOM when plugins were the future of web development.
The point is, a lot has changed. The browser can now handle DOM traversal in ways that weren’t possible when jQuery was invented. Plus, new libraries have come along that offer a totally different approach to DOM traversal than the one we’ve grown so accustomed to. So, it seems like a fair time to ask, do we even need jQuery for DOM traversal anymore?
How We Usually Do DOM Traversal
Before we explore the native methods available or alternative libraries. It’s useful for us to first remind ourselves of what the common DOM traversal methods that come with jQuery are, so that we know what it is we’ll need to replace.
Selectors
Of course, the first thing we need are some selectors. jQuery offered a CSS-like syntax for selecting DOM elements, which helped make it feel comfortable. Here are some simple selectors:
$("#item") // select an element with the id of "item"
$(".fancybutton") // select an element with a class of "fancybutton"
$("li:even") // select the even numbered items in a list
$("ul li:nth-child(3)") // select the 3rd list item in an unordered list
Most readers are probably already comfortable with the basics of selectors in jQuery. Once you’ve selected a DOM element, you can begin travering from that point – so let’s look at the typical DOM traversal methods.
DOM Traversal Methods
Before we begin exploring the various DOM traversal methods, it may be useful to have a simple DOM to work with, that way we can clearly illustrate what each method would do.
<html>
<head>
<title>DOM Traversal Sample</title>
<style>
.cell {
height: 50px;
width: 50px;
border: 1px solid #000000;
}
</style>
</head>
<body>
<table class="board">
<tr id="row1" class="row">
<td id="cell11" class="cell"></td>
<td id="cell12" class="cell"></td>
<td id="cell13" class="cell"></td>
</tr>
<tr id="row2" class="row">
<td id="cell21" class="cell"></td>
<td id="cell22" class="cell"></td>
<td id="cell23" class="cell"></td>
</tr>
<tr id="row3" class="row">
<td id="cell31" class="cell"></td>
<td id="cell32" class="cell"></td>
<td id="cell33" class="cell"></td>
</tr>
</table>
</body>
</html>
Our example is trivial, but this simple table of three columns and three rows makes it easy to visualize what we are selecting. Below are the most commonly used DOM traversal methods along with an illustration showing what they would select.
Note: In the illustrations, the highlighted element is the DOM node that is selected via the selector and the X’s indicate the nodes selected by the traversal method.
siblings()
The siblings()
method gets the sibling elements (i.e. DOM nodes with the same immediate parent element) of the selected node. In the case of a table cell, that would return the other adjoining cells.
$("#cell11").siblings()
In the case of a row, this would return the other rows.
$("#row1").siblings()
parent() and parents()
The parent()
method returns the immediate parent node of the selected DOM node. Thus, for a cell it would return the row parent.
$("#cell23").parent()
For a row, it would return the table parent.
$("#row3").parent()
The parents()
method returns all parents for the selected node all the way up the dom tree. Thus, for a cell it would return the row and the table (technically speaking, it will also return the body and the html nodes but for the purposes of the illustration, I’ve left those out).
$("#cell33").parents()
next() and nextAll()
The next()
method returns the sibling that immediately follows the selected node. For instance, if we select a cell, it would return only the next cell in the same row.
$("#cell21").next()
However, nextAll()
would return all of the subsequent cells that follow. This differs from siblings()
in that it does not return any siblings that precede the selected DOM node.
$("#cell21").nextAll()
prev() and prevAll()
The ‘prev()’ and ‘prevAll’ methods work identically to ‘next()’ and ‘nextAll()’ except they select the preceding siblings.
$("#cell23").prev()
$("#cell23"). prevAll()
closest() and find()
Some DOM traversal methods require a selector of their own. For example, the ‘closest()’ method will get the DOM element that matches the passed selector and is closest to the selected node within the DOM tree. It’s important to note that it does this by traversing up the DOM tree, meaning it won’t return any elements that are children of the currently selected node.
$("#cell32").closest(".row")
Unlike closest()
, which traverses up the DOM tree, find()
traverses down. It will return any elements that match the passed selector (unlike closest()
which only returns one).
$("#row2").find(".cell")
Do We Even Need jQuery?
It turns out that in the years since jQuery first came about, the browser has greatly improved in its ability to handle basic DOM traversal. In fact, under the covers, wherever possible, jQuery is just using many of these methods, meaning that, in many cases, we are replacing one line of JavaScript with one line of jQuery in our code – but with the added heft of the entire jQuery library.
Given these improvements, its fair to ask, do we even need jQuery at all for DOM traversal? Let’s examine how we might replace all of the code listed above using plain JavaScript.
Selectors
Using document.querySelector()
and document.querySelectorAll()
, we can easily replace the selectors I listed above. If you are so inclined, you could even map these functions to $
and you’d hardly notice jQuery was missing. Let’s see how.
Here are the replacements for the four selectors listed previously.
document.querySelector("#item")
document.querySelectorAll(".fancybutton")
//…pass!
document.querySelectorAll("ul")[0].children[2]
As you can see, it’s pretty much a one line of plain JavaScript for one line of jQuery replacement.
OK – So I Cheated!
…but only just a little.
Most selectors in jQuery are straight CSS selectors. Since document.querySelector()
also uses CSS selectors, the replacement is simple. However, some (like :even
) are jQuery specific. The following function is based upon the implementation of :even
within jQuery and will accomplish the same thing.
function selectEven(elems) {
var i = 0, length = elems.length, matches = [];
for ( ; i < length; i += 2 ) {
matches.push(elems[i]);
}
return matches;
}
selectEven(document.querySelectorAll("li"));
The method isn’t complicated, but it does mean you have to call selectEven()
as opposed to filtering with the :even
selector.
The above method will work for any element, but you could also, in cases where all the items are children of a particular element (which is commonly the case), replace with this one-liner:
document.querySelectorAll("ul li:nth-child(even)");
siblings()
As with a number of the DOM traversal methods, there is no native replacement for siblings()
, so we’ll need to replace it with a method of our own.
function getSiblings(elem) {
var i = 0, n = elem.parentNode.firstChild, matches = [];
for ( ; n; n = n.nextSibling )
if ( n.nodeType == 1 && n != elem)
matches.push( n );
return matches;
}
Once this method is written, we’d could relatively easily replace the jQuery examples from above.
getSiblings(document.querySelector("#cell11"))
getSiblings(document.querySelector("#row1"))
The returned elements would be the same.
parent() and parents()
There is a straightforward one line replacement for the jQuery parent()
method. The prior examples could be replaced with:
document.querySelector("#cell21").parentNode
document.querySelector("#row3").parentNode
Once again, these will return the same DOM node. However, there is no simple replacement for parents()
, so we’ll have to write one.
function getParents(elem) {
var n = elem.parentNode; matches = [];
for ( ; n; n = n.parentNode ) {
if ( n.nodeType == 9) {
return matches;
}
else if (n.nodeType == 1)
matches.push( n );
}
}
Using this method, we could replace jQuery’s parents()
with our getParents()
.
getParents(document.querySelector("#cell33"))
next() and nextAll()
There are actually numerous potential replacements for these methods (note: Thanks to Keith Clark for the tip). There is a nextSibling
property on Node, which you might think would work. This is the property used internally by jQuery to handle this method. However, in some browsers, this returns a blank space, which is often just there for code formatting, so a replacement needs to accommodate this.
For simplicities sake, I decided to replace both methods with a single method but it is based upon the way it is implemented inside jQuery.
function getNext(elem,all) {
var n = elem.nextSibling, matches = [];
for ( ; n; n = n.nextSibling ) {
if ( n.nodeType == 1) {
matches.push( n );
if (!all) return matches;
}
}
return matches;
}
To replace jQuery’s next()
we simple call getNext()
and supply the DOM node:
getNext(document.querySelector("#cell21"))
To replace nextAll()
, we simply supply the second parameter as true
:
getNext(document.querySelector("#cell21"),true)
However, it turns out that you can also get the next element using just specialized selectors. You can select the next element using the adjacent selector:
document.querySelector("#cell21+*")
Or recreate nextAll()
using the less strict sibling combinator.
document.querySelectorAll("#cell21~*")
In addition, the childNode API has a property called nextElementSibling
that will eliminate the behavior of returning whitespace elements. So next()
could be recreated as:
document.querySelector("#cell21").nextElementSibling
I have not done cross-browser testing on these methods but, according to MDN, the support is excellent (though I am not sure why jQuery doesn’t use it).
prev() and prevAll()
As you would expect, this is similar to the next()
replacement. We utilize previousSibling
(as jQuery does) but handle situations where it returns spaces.
function getPrev(elem,all) {
var n = elem.previousSibling, matches = [];
for ( ; n; n = n.previousSibling ) {
if ( n.nodeType == 1) {
matches.push( n );
if (!all) return matches;
}
}
return matches;
}
Once again, our replacement for the prior examples is simple. For prev()
, we pass just the DOM node:
getPrev(document.querySelector("#cell23"))
And for prevAll()
we pass the secondary argument as true
:
getPrev(document.querySelector("#cell23"),true)
As with next()
, the childNode API has a property called previousElementSibling
that will eliminate the behavior of returning whitespace elements. So prev()
could be recreated as:
document.querySelector("#cell23").previousElementSibling
Again, I have not done cross-browser testing on this method, but according to MDN, the support is excellent (though, again I am not sure why jQuery doesn’t use it).
closest() and find()
As you hopefully recall from earlier in the article, closest()
and find()
took additional selectors as arguments. There is also no direct replacement for closest()
but we can easily achieve one using the matchesSelector()
method.
function getClosest(elem, selector) {
var n = elem, el;
for ( ; n; n = n.parentNode ) {
el = n;
if ( n.matchesSelector(selector))
return el;
}
}
This method is pretty simple and works just fine. Simply pass the selected node as the first argument and the selector as the second and we have a replacement for the jQuery example:
getClosest(document.querySelector("#cell32"),".row");
The problem is that matchesSelector
support is not yet universal and is often prefixed (in fact, the actual spec appears to be matches
and not matchesSelector
but it hasn’t been implemented that way in most browsers). So, if you need to support legacy browsers and need to use closest()
, things get complicated.
On a positive note, however, replacing find()
is very simple.
document.querySelector("#row2").querySelectorAll(".cell")
You can chain selectors using querySelector()
and querySelectorAll()
, which is how we can easily replace the find()
jQuery example from earlier.
Maybe You Do Need jQuery!
While replacing any individual method wasn’t overly complex, we’ve now written six new methods just to replicate the basic jQuery examples from earlier in the article. Our methods may work, but overall its nowhere near as robust or easy to use as jQuery. So maybe it turns out that we do need jQuery after all!
…or do we? Someone must have thought of new approaches to DOM traversal in the eight years since jQuery arrived. Perhaps there a better, more modern way to do this that leverages of some of the browser API improvements.
DOM Traversal with HTML.js
Nearly a year ago, I wrote about a library called Voyeur. It dared to take a completely different approach to DOM traversal by allowing you to chain together DOM nodes in a very intuitive and comfortable fashion. For example, rather than selecting a node via $("ul li")
, you would simple use something more like ul.li
. This seemed to make complete sense in many ways.
And then it didn’t. The author of Voyeur, Adrian Cooney, abandoned the project because, as he explained it, it could never achieve the performance needed to actually use it in a production application (he specifically cited the performance around Object.defineProperty
as the primary issue).
But the story didn’t end there. Nathan Bubna picked up where Voyeur left off and created a library called HTML.js that worked to improve some of the issues with Voyeur.
Let’s look at how this approach is different and whether it might offer a better solution.
Selectors
The replacement for the selectors from my prior jQuery example are pretty straightforward since the query()
method of HTML.js works similarly to jQuery. Once again, the replacement for :even
is slightly convoluted, but at least we were able to get it to a single line (well, sort of).
HTML.query("#item")
HTML.query(".fancybutton")
HTML.query("li").only(function(o,i){ return !(i%2); })
HTML.query("ul").only(3)
Arguably, however, this is not an improvement nor is it demonstrably different than jQuery.
siblings()
We can also replace jQuery’s siblings()
with what can be called one line of JavaScript using HTML.js. The replacement for the jQuery examples from earlier would be:
HTML.query("#row1").div.only(function(o,i){ if (!o.matchesSelector("#cell11")) return o; })
HTML.query(".board").div.only(function(o,i){ if (!o.matchesSelector("#row1")) return o; })
Once again, the fact that this is a single line is more of a technicality than anything else and certainly can’t be called an improvement over jQuery.
parent() and parents()
Since HTML.js works with standard DOM nodes, we can rely on the standard methods provided by the browser for many functions. This means that, rather than have a method for parent()
, we simple use parentNode
.
HTML.query("#cell23").parentNode
HTML.query("#row3").parentNode
This could be considered a small improvement over the jQuery method but really isn’t an improvement over the straight JavaScript examples shown previously.
However, since there is no browser replacement for parents()
, we need to implement a method for it very similar to the straight JavaScript example from before.
function getParents(elem) {
var n = elem; matches = [];
for ( ; n; n = n.parentNode ) {
if ( n.nodeType == 9) {
return matches;
}
else if (n.nodeType == 1)
matches.push( n );
}
}
The only real difference here is that since we are using HTML.js, we pass a DOM node using that.
getParents(HTML.query("#cell33"))
Definitely not an improvement.
WTF! Now You’re Just Messing with Us!
Ok. I admit that so far, using HTML.js seems like a mix of the worst of both worlds. However, that’s really because we aren’t relying on its strengths and instead we’re trying to fit it into a jQuery box.
Where HTML.js excels is in traversing down the DOM tree in an intuitive way using chaining of DOM elements.
HTML.body.table.tbody.tr.td
The result of this would be all the table cells in the first row.
Our simple example HTML did not actually have the tbody
tags in it, but in my testing these were automatically added to the DOM (at least in Chrome) and thus were necessary.
HTML.js offers some methods to easily filter the results.
HTML.body.table.tbody.tr.only(1)
This would select the second row in the table (unlike the jQuery or straight JavaScript selectors, this is using a zero-based array).
You can continue chaining as necessary to get the results you want.
HTML.body.table.tbody.tr.only(1).query(".cell")
HTML.js also provides useful methods like each()
for traversing through and acting upon the results.
HTML.body.table.tbody.tr.query(".cell").each(function(el, i, all) {
// do something with each table cell node
});
For example,the following code:
HTML.query("body > table tr td.cell").each('textContent','X')
…would place an “X” inside every cell in the table.
One thing I did notice was that, while it was very easy to move down the DOM, it was much less easy to move back up. In theory, you could rely upon built-in methods like previousSibling
or parent
, but as we’ve already covered, these offer some complications which you would still need to resolve.
Another complication is that it doesn’t offer a complete set of alternatives for the jQuery DOM traversal methods we covered previously.
Still, I feel it’s worth experimenting with HTML.js as at least offers a fresh perspective on the task of DOM traversal.
Wrapping Up
Based upon my experiments using jQuery, plain JavaScript and HTML.js, here are my conclusions.
- jQuery still offers the easiest and cleanest API for DOM traversal…however, jQuery might be more than you need.
- In many cases, you can replace 1 line of jQuery with 1 line of plain JavaScript…however, it’s not always so easy and you may end up rewriting (and maintaining) a lot of methods.
- HTML.js offers a unique way to traverse the DOM that can feel intuitive in many cases…however, it’s missing a lot of methods that you might need.
In my opinion, the net result is that most people should probably just stick with jQuery. It’s API is still the most complete and the so-called “bloat” is often overstated.
First, few improvements to the article:
It’s a parentNode property, not a parentNode() function. This is one of the main reasons i’m pushing back against jQuery. People aren’t learning the DOM well, just a wrapper API.
You are better off adding a proper siblings function. May i suggest:
function () {
var self = this;
return HTML.ify(self.parentNode.children).filter(function(el){ return el !== self; });
}
You missed the power of each(). This is much cleaner:
HTML.(“body > table tr.cell”).each(‘textContent’, ‘X’);
But my real issue with this article is that it comparing things with jQuery on jQuery’s terms. This *is not* rethinking anything about DOM traversal. This appears to be about checking to see if jQuery is necessary/replaceable.
That said, for those reading this. I’ve been spending a lot of time actually rethinking DOM interaction (traversal and manipulation) since well-before Voyeur inspired me to fork it into HTML.js. There are two strong conclusions, i have:
1) The DOM is no longer a mess. The messy DOM of old birthed jQuery’s alternate API and chased people away from actually using and extending it. But things have changed. The native APIs are becoming more consistent and useable with each passing month, and DOM extension is no longer evil. HTML.js, i think, has proved that well, but it didn’t really take it far enough (more on this in a moment).
2) jQuery is holding a lot of people back. As you’ve evidenced above, people think in jQuery. They know it’s API and not the native ones that it is using. The fewer people know and use the native DOM, the fewer people will advance it and tools that use it. And of course, the jQuery ecosystem has always propped up old IE and old browsers in general. That’s an upside for simple web sites, i believe, but for rich web apps, i think it does more harm than good.
Anyway, i don’t mean to bash. I just get excited when i hear people talk about rethinking DOM interaction, so it’s disappointing to find jQuery used as the baseline, not merely the current dominant pattern. Perhaps i need to write a version of this myself. If i did, it may interest you to know, i probably wouldn’t highlight HTML.js as much, as i’ve begun using a newer project instead. It’s not ready for publicity though.
I fixed the typo on the parentNode. I also updated the each() example (I think it was important to show it both ways to better illustrate the API).
As for not really rethinking the DOM. I do note in the discussion about HTML.js that, in fact, trying to use it like jQuery is a mistake. That being said, the methods in jQuery I illustrate were chosen because they support common requirements for DOM traversal. This doesn’t mean that you need to recreate jQuery’s API as I have here, but it does mean you need to know how to achieve similar results. Using jQuery as the baseline makes sense if you plan to get anyone on board since, as of today, they are all using jQueyr. I’d love to hear your take on this in more detail – perhaps you’d be willing to write a follow up 🙂
I’ll think about it. It might be a good way to lead off publicizing the new project, but first i need to finish tests and docs for that.
Oh, one more note for the article. HTML.js makes the ‘matches’ function available under the proper name (polyfill-by-rename, if you will). Because the only() function requires consistent access to it. So you can shorten your sibling functions, even if you don’t want to use the HTML._.fn.siblings implementation i suggested.
And regarding the update about jQuery not using previous/nextElementSibling internally… Unless they know something i don’t, to keep them from using it, then that’s just the kind of surprising ignorance about the native DOM that i’m talking about.
It seems more likely to me that there are scenarios jQuery needs to support where this doesn’t work. But I’m just guessing.
Was thinking about this during lunch, and realized your getParents(el) function would be much simpler if you use parentElement instead of parentNode, as you would not need to check the nodeType.
Also, i think i’ll add an “all(prop[, selfIncluded])” function to HTML.js (and the successor i’m working on) that lets you do: el.all(‘parentElement’) or el.all(‘nextElementSibling’) or any other DOM tree-walking property. That should make most of these trivial, while still teaching/using native DOM. Only siblings() might remain a bit tricky, but i’m not yet convinced that one is a common enough use case (nor that difficult enough to work around) to be worth including. 🙂
all(property[, includeSelf]) has landed in HTML.js 0.12.0
https://nbubna.github.io/HTML/#all(property)
Oh, and your siblings() implementation for HTML.js could be shortened dramatically:
HTML.query(“#row1”).div.only(“:not(#cell11)”)
Your definition of closest is slightly wrong. If the current node matches the selector, closest will return itself. What you described would be equivalent to calling parents with :first on the end of the selector.
The whole article, you try really hard to find an alternative and then the very last sentence, you say, “stick with jQuery.”
Indeed, it’s going to be very hard to overcome jQuery. It does what it does and it does it really well.
On top of that, if it’s “too much” for you, think about giving custom jQuery builds (https://github.com/jquery/jquery#modules) a try and you can cut out the stuff you don’t need. At ~28k gzipped & minified (the full source), it is about 10x bigger than HTML.js, but there are usually simpler things to get rid of that extra ~26k than looking for jQuery alternatives. Heck, use Zepto, weighing in at ~9.2k.
Additionally, the way the dot-syntax is presented in HTML.js, that’s a horrible way of doing it. It is painfully explicit and any slight change in your DOM would have the potential to literally breaking everything. Using > in selectors once is generally a bad idea unless you have a very specific reason to, and that dot-syntax seems to be throwing in a million > to the mix.
I’m sure you’ve seen https://youmightnotneedjquery.com/
But maybe you still do? https://gist.github.com/rwaldron/8720084#file-reasons-md
Yeah, that’s why HTML.js only recommends dot-traversal for small structures (things like ul.li and select.option), and has the HTML.query method. https://nbubna.github.io/HTML/#FAQ
And the push against jQuery was not primarily because it was “too much” or “too big”. It’s that it wraps the DOM. Hides it unnecessarily. It creates a separate API and ecosystem that once made us all rich, but now is holding us back. And if you read through Rick’s list of “browser bugs” that jQuery arguably works around, i don’t think it will actually make you feel the need for jQuery’s protection. It didn’t have that effect on me. In fact, there are plenty of duplicates in there, things that i suspect only matter for older versions, and things that really aren’t a problem. The DOM is not the mess that it was. jQuery isn’t doing a great job internally, even, of keeping up with available features and fixes. It was a great exoskeleton for letting us maneuver around the messy DOM of past years, but now, it’s mostly dead weight. It’s time to befriend (and extend!) the DOM again.
Oh, and here’s an analysis of rwaldron’s list: https://etherpad.mozilla.org/jquery-browser-bug-analysis
Summary, lots of sizzle stuff (don’t need it, just use querySelectorAll), old IE, and Android 2.3, etc. And some are non-DOM traversal or have reasonable workarounds or are obscure. jQuery is not saving you from all kinds of scary DOM issues. Things have changed. And what few things it spares you from aren’t worth the cost (to the dev community as a whole, especially) of hiding the DOM behind a wrapping API.
jQuery set precedence for clunky, clumsy over-reliance on blind DOM traversal in place of actual business logic. There are probably only very, very few great uses for .next() out in the wild.
Select the elements you will need, cache them, and then proceed. To do that in new-world UAs, you never need jQuery.
jQuery is based on Sizzle ( https://sizzlejs.com/ ), they actually use the source.
If you don’t like jQuery for any reason, why don’t you give sizzle a try?
Dennis, Sizzle is just the selector engine that jQuery uses to find elements, and in most cases it’s now replaced by native querySelector calls. It is not a simpler, or “base” version of jQuery and doesn’t offer any functionality beyond finding elements in the DOM.
Good article.
Let’s think about the abstraction magic in S/W development. When it comes to the selectors, jQuery abstracts most of the plain old javascript api. Then developers can do programming much easier with the abstractions across all sort of browsers. It can be neglected for developers to know which native JS apis are supported or not on specific browsers.
Hi guys, wanted to share tiny library for dom traversal based on lodash with you – https://github.com/szarouski/lodash.dom-traverse. It includes only necessary minimum – you could do everything else with native dom support. IE8+.
To render justice where it is due to jQuery. We have to remind the reader, there is a stable jQuery 2.x still in constant evolution. Those versions are not cross-browser compatible with IE < 9. And more and more, tend to use the proposed MDN implementations you cited in your article. More and more, jQuery is doing that one line conversion you want to sell us, but continue to help the developper forget about the current vendor prefixes and caveats in each modern browser.
Finally, jQuery does it right in the long run. I'm not sure we will go back to native JS one day – atleast for big implementations. Before that happen, the browsers will part ways. You know they wont follow the same standard indefinatly.
As a developper, i learn several ways to do DOM traversal, with native JS included. Any dev should. I still rely on jQuery though. Less hassle with compatibility.