Lose the jQuery Bloat ­— DOM Manipulation with NodeList.js

Share this article

In recent years, jQuery has become the de-facto JavaScript library on the web. It irons out many cross-browser inconsistencies and adds a welcome layer of syntactic sugar to client-side scripting. One of the main pain points it abstracts away is DOM manipulation, but since its inception, native browser APIs have improved dramatically and the idea that You May Not Need jQuery has started to gain in popularity.

Here are some reasons why:

  1. jQuery includes a bunch of features that you don’t need or use (so the weight is unnecessary).
  2. jQuery is too many things to too many people. Often smaller libraries can accomplish certain tasks better.
  3. In terms of DOM manipulation, browser APIs can now do most of what jQuery can.
  4. Browsers APIs are more in sync now, e.g. using addEventListener instead of attatchEvent.

So What’s the Problem?

The problem is that DOM manipulation using vanilla (or plain) JavaScript can be a pain compared to jQuery. This is because you have to read and write more redundant code, and deal with the browser’s useless NodeList.

First let’s look at what a NodeList is according to MDN:

NodeList objects are collections of nodes such as those returned by Node.childNodes and the document.querySelectorAll method.

And sometimes there are live NodeLists (which can be confusing):

In some cases, the NodeList is a live collection, which means that changes in the DOM are reflected in the collection. For example, Node.childNodes is live.

This can be a problem because you can’t tell which are live, and which are static. Unless you remove each of the nodes from the NodeList and then check if the NodeList is empty. If it is empty then you have yourself a live NodeList (which is just a bad idea).

Also the browser doesn’t provide any useful methods to manipulate these NodeList objects .

For example, unfortunately it isn’t possible to loop through the nodes with forEach:

var nodes = document.querySelectorAll('div');
nodes.forEach(function(node) {
  // do something
});
// Error: nodes.forEach is not a function

So you have to do:

var nodes = document.querySelectorAll('div');
for(var i = 0, l = nodes.length; i < l; i++) {
  var node = nodes[i];
  // do something
}

Or are even left with using a “hack”:

[].forEach.call(document.querySelectorAll('div'), function(node) {
    // do something
});

The browser’s native NodeList only has the one method: item. This returns a node from a NodeList by index. It is completely useless when we can access that node just like we would with an array (using array[index]):

var nodes = document.querySelectorAll('div');
nodes.item(0) === nodes[0]; // true

That’s where NodeList.js comes in — to make manipulating the DOM with the browser’s native APIs as easy as it is with jQuery, but for only 4k minified.

The Solution

I created NodeList.js because I’ve always used the native DOM APIs, but wanted to make them more terse, so as to remove lots of the redundancy when writing my code (e.g. for loops).

NodeList.js is a wrapper around the native DOM APIs which allows you to manipulate an array of nodes (AKA my NodeList) as if it were a single node. This gives you much more functionality than the browser’s native NodeList objects.

If this sounds good to you, go grab a copy of NodeList.js from the official GitHub repo and follow along with the rest of this tutorial.

Usage:

Selecting DOM nodes is simple:

$$(selector); // returns my NodeList

This method uses querySelectorAll(selector) under the hood.

But How Does It Stack up against jQuery?

Glad you asked. Let’s put vanilla JS, jQuery and NodeList.js head to head.

Let’s say we have three buttons:

<button></button>
<button></button>
<button></button>

Let’s change the text of each button to “Click Me”:

Vanilla JS:

var buttons = document.querySelectorAll('button'); // returns browser's useless NodeList
for(var i = 0, l = buttons.length; i < l; i++) {
  buttons[i].textContent = 'Click Me';
}

jQuery:

$('button').text('Click Me');

NodeList.js:

$$('button').textContent = 'Click Me';

Here we see that NodeList.js can effectively treat a NodeList as a single node. That is to say, we have reference to a NodeList and we just set its textContent property to “Click Me”. NodeList.js will then do this for each node in the NodeList. Neat, huh?

If we wanted method chaining (à la jQuery) we would do the following which returns a reference to the NodeList:

$$('button').set('textContent', 'Click Me');

Now let’s add a click event listener to each button:

Vanilla JS:

var buttons = document.querySelectorAll('button'); // returns browser's useless NodeList
for(var i = 0, l = buttons.length; i < l; i++) {
  buttons[i].addEventListener('click', function() {
    this.classList.add('clicked');
  });
}

jQuery:

$('button').on('click', function() {
  $(this).addClass('click');
  // or mix jQuery with native using `classList`:
  this.classList.add('clicked');
});

NodeList.js:

$$('button').addEventListener('click', function() {
  this.classList.add('clicked');
});

Ok, so the jQuery on method is fairly nice. My library uses the browser’s Native DOM APIs (hence addEventListener), but it doesn’t stop us creating an alias for the method:

$$.NL.on = $$.NL.addEventListener;

$$('button').on('click', function() {
  this.classList.add('clicked');
});

Nice! And this demonstrates exactly the way we would add our own methods:

$$.NL.myNewMethod = function() {
  // loop through each node with a for loop or use forEach:
  this.forEach(function(element, index, nodeList) {...}
  // where `this` is the NodeList being manipulated
}

NodeList.js on Array Methods

NodeList.js does inherit from Array.prototype, but not directly, as some methods are altered so that it makes sense to use them with a NodeList (an array of nodes).

Push and Unshift

For example: the push and unshift methods can only take nodes as arguments, or they will throw an error:

var nodes = $$('body');
nodes.push(document.documentElement);
nodes.push(1); // Uncaught Error: Passed arguments must be a Node

So both push and unshift return the NodeList to allow method chaining, meaning it’s not the same as JavaScript’s native Array#push, or Array#unshift methods, which accept anything and return the new length of the Array. If we did want the length of the NodeList we just use the length property.

Both of these methods, just like JavaScript’s native Array methods, do alter the NodeList.

Concat

The concat method will take the following as arguments:

  • Node
  • NodeList (both the browser’s native one, and NodeList.js version)
  • HTMLCollection
  • Array of Nodes
  • Array of NodeList
  • Array of HTMLCollection

concat is a recursive method, therefore those arrays can be as deep as we like and will be flattened. However if any of the elements in the passed arrays are not of Node, NodeList, or HTMLCollection it will throw an Error.

concat does return a new NodeList just like javascript’s Array#concat method does.

Pop, Shift, Map, Slice, Filter

The pop and shift methods can both take an optional argument as to how many nodes to pop or shift from the NodeList. Unlike JavaScript’s native Array#pop or Array#shift where will always pop or shift one element from the array regardless of what is passed as an argument.

The map method will return a NodeList if each mapped value is a Node, or an array of the mapped values if not.

The slice and filter methods act just like they do on real arrays, yet will return a NodeList.

Since NodeList.js does not directly inherit from Array.prototype if a method is added to the Array.prototype after NodeList.js is loaded, it won’t be inherited.

You can check out the rest of NodeList.js array methods here.

Special Methods

There are four methods unique to NodeList.js, as well as a property called owner, which is the equivalent of jQuery’s prevObject property.

The get and set Methods:

There are some elements with properties unique to that kind of element (e.g. the href property on an anchor tag). This is why $$('a').href will return undefined — because it’s a property that not every element in the NodeList inherits. This is how we would use the get method to access those properties:

$$('a').get('href'); // returns array of href values

The set method can be used to set those properties for each element:

$$('a').set('href', 'https://sitepoint.com/');

set also returns the NodeList to allow method chaining. We can use this on things like textContent (both are equivalent):

$$('button').textContent = 'Click Me';

$$('button').set('textContent', 'Click Me'); // returns NodeList so you can method chain

We can also set multiple properties in one call:

$$('button').set({
    textContent: 'Click Me',
    onclick: function() {...}
});

And all of the above can be done with arbitrary properties, such as style:

$$('button').style; // this returns an `Array` of `CSSStyleDeclaration`

$$('button').style.set('color', 'white');

$$('button').style.set({
    color: 'white',
    background: 'lightblue'
});

The call Method

The call method allows you to call those methods unique to an element (for example pause on a video element):

$$('video').call('pause'); // returns NodeList back to allow Method Chaining

The item Method

The item method is the equivalent of jQuery’s eq method. It returns a NodeList containing only the node of the passed index:

$$('button').item(1); // returns NodeList containing the single Node at index 1

The owner Property

The owner property is the equivalent of jQuery’s prevObject.

var btns = $$('button');
btns.style.owner === btns; // true

btns.style returns an array of styles and owner gives you back the NodeList which style was mapped from.

NodeList.js Compatability

My library is compatible with all of the major new browsers, as detailed below.

Browser Version
FireFox 6+
Safari 5.0.5+
Chrome 6+
IE 9+
Opera 11.6+

Conclusion

Now we can finally work with a useful NodeList object!

For about 4k minified you get all the functionality mentioned above, and plenty more which you can learn all about in the GitHub repository of NodeList.js.

Since NodeList.js uses the browser as a dependency, there won’t be any upgrading to do. Whenever browsers add new methods/properties to DOM elements, you’ll automatically be able to use those methods/properties via NodeList.js. All of which means the only deprecation you’ll ever need to worry about are the methods that browsers get rid of. These are usually ones that are in very low use, because we can’t break the web.

So what do you think? Is this a library that you’d consider using? Are there any important features missing? I’d love to hear from you in the comments below.

Frequently Asked Questions about DOM Manipulation with NodeList.js

What is the difference between NodeList and HTMLCollection?

NodeList and HTMLCollection are both collections of nodes. The main difference between them is that NodeList can contain any node type, while HTMLCollection is a collection of element nodes. HTMLCollection is also live, meaning it automatically updates when the document structure changes. On the other hand, NodeList is static and does not update to reflect changes in the document.

How can I convert a NodeList to an array?

You can convert a NodeList to an array using the Array.from() method or the spread operator. Here’s how you can do it:

let nodeList = document.querySelectorAll('div');
let array = Array.from(nodeList);
// or
let array = [...nodeList];

Why does jQuery selector return prevObject instead of a normal element?

jQuery’s chaining mechanism works by storing the previous object before making a change. This allows you to revert to the previous state using the .end() method. If you want to get the actual DOM element, you can use the .get() method or array notation.

How can I loop through a NodeList?

You can loop through a NodeList using a for loop, a for…of loop, or the forEach() method. Here’s an example using a for loop:

let nodeList = document.querySelectorAll('div');
for (let i = 0; i < nodeList.length; i++) {
console.log(nodeList[i]);
}

What is the use of the .prev() method in jQuery?

The .prev() method in jQuery is used to select the immediately preceding sibling element of the selected element. If a selector is provided, it retrieves the previous sibling only if it matches that selector.

Is jQuery still relevant in 2022?

While jQuery was a game-changer when it was released, the modern JavaScript ecosystem has evolved significantly. Many of the features that made jQuery popular are now built into JavaScript itself. However, jQuery is still widely used and maintained, and it can be a good choice for certain projects.

How can I select a specific node from a NodeList?

You can select a specific node from a NodeList using array notation or the item() method. Here’s how you can do it:

let nodeList = document.querySelectorAll('div');
let firstNode = nodeList[0];
// or
let firstNode = nodeList.item(0);

Can I use map, filter, and reduce methods with NodeList?

NodeList is not an array, so it does not have methods like map, filter, and reduce. However, you can convert a NodeList to an array and then use these methods.

What is the difference between querySelector and querySelectorAll?

querySelector returns the first element that matches a specified CSS selector in the document, while querySelectorAll returns a NodeList of all elements that match the CSS selector.

How can I check if a NodeList is empty?

You can check if a NodeList is empty by checking its length property. If the length is 0, the NodeList is empty. Here’s how you can do it:

let nodeList = document.querySelectorAll('div');
if (nodeList.length === 0) {
console.log('NodeList is empty');
}

Edwin ReynosoEdwin Reynoso
View Author

I'm a very young web developer, who's passionate about improving the native web platform over tools, so that the web as a whole improves. I love hacking the DOM. That's probably due to most people hating the DOM and its how I got into JavaScript.

array methodsDOM manipulationjameshjQueryNodeListvanilla javascript
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week