DOM Traversal and Manipulation with Voyeur

by Brian Rinaldi on July 22, 2013

The modern web is always changing, and this article is more than two years old.

Voyeur-header

By Brian Rinaldi

Years ago, just about the only thing cool about JavaScript was the DOM traversal and manipulation that jQuery offered. Even today, it remains one of the most powerful aspects of the jQuery library. The fact that the syntax so closely replicates CSS selectors makes it relatively easy to use and learn.

Nonetheless, it is possible that there are alternative means of selecting and manipulating the DOM that could offer potential benefits. In addition, the native DOM selection and manipulation capabilities of the browser has grown enormously. Recently, Adrian Cooney released a new library called Voyeur that offered an alternative. In this article we’ll take a look at how to do some common tasks within the DOM using Voyeur. One of the interesting things, to me at least, is the way Voyeur mixes some basic DOM traversal methods while still relying heavily on the browser for many tasks.

Obviously, jQuery does far more than just DOM traversal and manipulation, but sometimes that is all you need. However, before you jump in and replace jQuery with Voyeur, be sure to read the final sections as there are some important caveats to consider.

Sample DOM

For the sake of our examples, I have made a very simple HTML page based upon a generated template from Initializr. I’ve made some simple edits to the generated HTML. For the purposes this test, we’re only messing with the DOM, so only the HTML matters, which you can see below.

<!DOCTYPE html>
<html class="no-js">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">

        <link rel="stylesheet" href="css/normalize.min.css">
        <link rel="stylesheet" href="css/main.css">

        <script src="js/vendor/modernizr-2.6.2.min.js"></script>
    </head>
    <body>

        <div class="header-container">
            <header class="wrapper clearfix">
                <h1 class="title">Voyeur.js Sample Site</h1>
                <nav>
                    <ul>
                        <li><a href="http://dunxrion.github.io/voyeur.js/">Voyeur Site</a></li>
                        <li><a href="https://github.com/dunxrion/voyeur.js">GitHub</a></li>
                        <li><a href="https://github.com/dunxrion/voyeur.js/blob/master/Voyeur.min.js">Download</a></li>
                    </ul>
                </nav>
            </header>
        </div>

        <div class="main-container">
            <div class="main wrapper clearfix">

                <article>
                    <header>
                        <h1>What is Voyeur?</h1>
                        <p>Voyeur is a tiny Javascript library that lets you traverse and manipulate the DOM. Voyeur allows you to traverse the DOM via the dot operator like you would any Javascript object.</p>
                    </header>
                    <section>
                        <h2>Browser Support</h2>
                        <p>Voyeur is tested in and supports all the latest, major browsers (IE10, Chrome, Firefox, Safari, Opera).</p>
                    </section>
                    <footer>
                        <h3>Get Voyeur</h3>
                        <p>You can find more information about Voyeur via <a href="http://dunxrion.github.io/voyeur.js/">its website</a> or <a href="https://github.com/dunxrion/voyeur.js/blob/master/Voyeur.js">create a fork on GitHub</a>.</p>
                    </footer>
                </article>

                <aside>
                    <h3>Element creation</h3>
                    <p>One of the most interesting aspect of Voyeur is its element creation interface. You can create and append elements by simply naming and chaining them. Voyeur works by utilizing object getter functions to expose an element's children via their tag names on itself.</p>
                </aside>

            </div> <!-- #main -->
        </div> <!-- #main-container -->

        <div class="footer-container">
            <footer class="wrapper">
                <p>A sample created by Brian Rinaldi using <a href="http://www.initializr.com/">Initializr</a> because I am lazy.</p>
            </footer>
        </div>

        <script src="js/Voyeur.js"></script>
        <script src="js/main.js"></script>
    </body>
</html>

You can reference this as we discuss the examples throughout the article. The page looks like the screenshot below.

voyeur-screen-01

Selecting DOM Nodes

Traversing and selecting DOM nodes always starts with entering Voyeur. Voyeur selects the body and you work your way from there. One thing to note, which seems trivial but isn’t really: Voyeur is really easy and common to mistype. Since, as you use this library, you reference it a lot, you might consider assigning it to an easier to type variable name.

As a basic example of traversal, in the HTML above, Voyeur.div selects the array of div elements directly within the body (there are 3). This is a native JavaScript array, so you can use it as such. As we’ll see later on, there are other means of selecting and looping through elements within Voyeur.

You might just assume that you could then select Voyeur.div.header to grab the header div within the first div element. This will give you just undefined though since Voyeur isn’t sure which div you are referencing. Granted, it could be made more obvious where you went wrong – many mistakes you might commonly make using Voyeur end up with undefined or TypeError. You’ll end up performing a lot of trial and error in the browser console.

eq()

So, how would you select the header under the initial div? The eq() method in Voyeur allows you to specify an item within an array of DOM elements. For example, Voyeur.div.eq(0).header selects the header within the first div element (as the array is zero based).

In the next example, we use eq() along with find() (which we discuss further in a moment) to select only the middle navigation element (which is a set of 3 list elements).

Voyeur.find(".header-container").header.nav.ul.li.eq(1);

find()

The find() method allows you to specify a class or ID selector. This works similarly to jQuery. As our example DOM has no ID’s, let’s see how to use a class-based selector.

Voyeur.find(".header-container").header;

The above line of code selects the same header element within the initial div as the previous example. You can scope find() as well as you can see below.

Voyeur.div.find(".main").article;

The previous line of JavaScript only searches for items with a class of “main” within the div elements under the body. The point being, if there was a “main” element outside of that scope, it would not be selected.

Looping Through Nodes

Voyeur provides a couple of methods to make it easy to loop through all returned DOM nodes or only those you request. Let’s look at how.

use()

When an array of DOM nodes is returned, the use() method within Voyeur lets you loop through each one, specifying a callback method that will run on each iteration. For example, the following script will change the labels for each navigation item within our sample page. Notice that I am able to extend the DOM traversal using Voyeur on each returned list element.

// this is just an array of menu labels
var labels = ['This','Is','Awesome!'];
Voyeur.find(".header-container").header.nav.ul.li.use(function(li, i) {
    li.a.innerText = labels[i];
});

When run, our navigation now looks like the following screenshot.

voyeur-screen-2

In the next example, we loop through every div child element of the body. If the div contains a child element of the type footer we’ll change the background color to red.

Voyeur.div.use(function(div,i) {
    if (div.footer)
        div.footer.style.background = "red";
});

As you can see with the prior example, you can manipulate the style of elements using the native browser methods that are available.

eq()

We’ve already seen how you can use eq() to isolate a single element within an array of returned values. It can also be used to specify a subset of the array. In the following code, we grab only the second and third navigation elements and loop through those to change their label text.

// this is just an aray of menu labels
var labels = ['GH','DL'];

// start with the second nav element and loop to the third element
Voyeur.find(".header-container").header.nav.ul.li.eq(1,3).use(function(li, i) {
    li.a.innerText = labels[i];
});

Running the prior code, our navigation now looks like the following screenshot.

voyeur-screen-3

Manipulating Nodes

As is obvious in the documentation, Voyeur doesn’t really have any built-in DOM manipulation methods. However, modern browsers already support much of what you need to combine with Voyeur. I’ll admit that I found it slightly difficult trying to figure out the available method from time to time to achieve the result I wanted (often through trial and error). Also, I should note that these examples have only really been tested in Chrome. Assuming you were intent on using Voyeur, you might need to verify the browser compatibility of whatever native methods you choose to employ for DOM manipulation.

The following line of code uses the innerHTML property to change the title text inside the h1 element within the header of our sample page (note that we could also have used innerText).

Voyeur.find(".header-container").header.h1.innerHTML = "I Changed This";

In the next example we’ll delete the third (final) list item in the nav by selecting it with Voyeur and using the remove() method in the browser.

Voyeur.find(".header-container").header.nav.ul.li.eq(2).remove();

In this example, we replace the onclick handler for the third navigation element so that when it is clicked it spawns an alert box while disabling the existing link.

Voyeur.find(".header-container").header.nav.ul.li.eq(2).onclick = function() {alert("foo");return false;}

Finally, we’ll modify the style of the text within the aside element.

Voyeur.find(".main-container").div.aside.style.fontSize="11px";

Creating Nodes

Voyeur includes a create() method that is, quite obviously, used for creating DOM elements. You can either create the element or elements and assign them to a variable to be inserted later into the DOM or you can use Voyeur to directly insert them into the DOM at the time of creation. In both cases, you can chain the creation of elements off a single create() method call. We’ll look at examples of both here.

In the following example, we are creating a node from scratch, assigning it to a variable and then inserting that node into the existing DOM. In this particular case, we are removing the third navigation element and then creating a new node that will replace it.

//delete the existing menu item
Voyeur.find(".header-container").header.nav.ul.li.eq(2).remove();

// recreate it
newnode = Voyeur.create.li.a;
newnode.href = "http://flippinawesome.org";
newnode.innerText = "Awesome!";
Voyeur.find(".header-container").header.nav.ul.appendChild(newnode.parentNode);

Notice that, as I am creating the anchor element inside the list item using chaining, I need to append the parent node (i.e. the list item). It will still contain the anchor element with a link for our new navigation item. Running this example makes our navigation look like the following screenshot.

voyeur-screen-4

In the next example, we accomplish the exact same task but create the DOM element inline. I am still assigning the new node to a variable so that I can modify the attributes of our anchor.

//delete the existing menu item
Voyeur.find(".header-container").header.nav.ul.li.eq(2).remove();

// recreate it
newnode = Voyeur.find(".header-container").header.nav.ul.create.li.a;
newnode.href="http://flippinawesome.org";
newnode.innerText = "Awesome!";

While the same result is achieved with both these examples, there are use cases that would require one or the other in order to get the desired result.

Our next example utilizes the mult() function within Voyeur to create multiple items at a time. Then we call the use() function discussed earlier to create a callback method when iterating through the newly created items. In this code, we remove all of the menu nodes and then recreate them with new text and new links.

//delete all the existing menu items
Voyeur.find(".header-container").header.nav.ul.innerHTML = "";

// recreate them
// this is just an aray of menu labels and then menu links
var labels = ['This','Is','Awesome!'];
var links = ['http://remotesynthesis.com','http://adobe.com','http://flippinawesome.org'];
nodearr = Voyeur.find(".header-container").header.nav.ul.create.li.mult(3).use(function(li, i) {
    newnode = li.create.a;
    newnode.href=links[i];
    newnode.innerText = labels[i];
});

Running this code will make our navigation look like the following screenshot, but will also create new links for each of the navigation elements.

voyeur-screen-5

In addition to the methods discussed here, Voyeur has a Voyeur.create.special() method that is used specifically to create style and script elements.

Comparing to jQuery

Obviously the list of examples above are not comprehensive, but you can hopefully see that it is possible to achieve most, if not all, of your DOM selection and manipulation needs using this library. From this standpoint, it would be just a matter of which syntax you prefer to use, jQuery’s or Voyeur’s. Also, Voyeur is rather small while jQuery, with a much broader scope overall, is rather large. So, you might then ask, “If I’m primarily using it for these tasks, can I simply replace jQuery with Voyeur?”

From a pure functionality perspective, it would seem possible. However, there’s a potentially large caveat, coming directly from the author of Voyeur himself:

Voyeur’s performance is bad. It’s so bad, Voyeur should never be used in production. It’s manages just over 11,000 operations per second on it’s most basic traversal in comparison to jQuery’s 300,000 operations per second.

This is according to details he listed on a GitHub issue for the project. Obviously, as some commenters pointed out, most use cases wouldn’t require that many traversal operations per second. However, these sorts of tests that iterate over large numbers of operations aren’t intended to necessarily replicate real-world environments but rather to better illustrate performance differences that become more apparent at these levels.

Clearly, from a performance standpoint, jQuery is the way to go. However, it is possible than within the limited scope of a particular project, Voyeur’s performance might be adequate enough to meet your needs.

Where To Go From Here?

So, if Voyeur’s performance isn’t quite up to snuff yet (and the author isn’t planning additional development at the moment), are we simply left with the status quo? Well, yes…and no. While it might not be recommended that you choose to use Voyeur today in your project, there are things that may change and, if you are interested, potentially ways you can contribute to that change.

Firstly, as Adrian notes in the GitHub issue mentioned earlier, the main performance issue of Voyeur is related to the overall poor performance of Object.defineProperty within browsers. Should this improve in future browser versions, the project may become much more viable.

Secondly, if you are particularly enamored with the syntax and scope of Voyeur, you can contribute to or even fork the project on GitHub. Perhaps you will be able to help resolve or at least improve this issue to a level you find acceptable (some commenters on the issue above seem to have already headed down this path).

Even if neither of those solutions seems worthwhile to you, I personally find the exercise at looking at alternative syntaxes or solutions to common problems a worthwhile endeavour. Even if Voyeur never becomes the solution, it might inspire someone to take a fresh look at how to handle DOM traversal and manipulation…and who knows where that can lead?

11 comments"

  1. tomByrer says:

    Thanks for the detailed article!
    I like the notation alot, but the speed could be improved… by 20 fold at least. http://jsperf.com/voyeur8000/3

    1. Nathan Bubna says:

      My fork of Voyeur (HTML) has achieved a drastic speed-up while still using Voyeur-type technique: http://jsperf.com/voyeur8000/5

  2. daniel says:

    I was hoping to hear that Voyeur might be faster than jQuery. If jQuery offers improved performance AND better features, it’s hard to see where Voyeur.js would be useful.

    1. remotesynth says:

      jQuery is definitely faster. However, this library is new and, as I note in the article, perhaps with some help and contributions, the performance could improve. For many basic tasks though, the performance seemed fine (at least on desktop).

      As for “better features,” that I disagree with. It all depends on what you are looking for. If what you need is primarily DOM manipulation then Voyeur has the features you need without any extra weight and fluff. Plus, it has an interesting, and perhaps to some preferable, syntax.

    2. Nathan Bubna says:

      Voyeur (and my HTML.js fork of it) is slower at browser traversal/selection, but when working with nodes themselves ought to be much faster, as it does not wrap the internal APIs but works with them directly. So, simply hold on to the selected nodes in loops (instead of repeating the selection/traversal over and over) and the performance begins to tilt quickly toward the library that keeps you close to the native DOM.

  3. Clark says:

    “faster” is also a very general term. If page load time is the bottleneck in your app – then Voyeur is much “faster” than jQuery because it is smaller. For example, It could be the better choice for use in third party widgets with relatively few DOM operations.

  4. Nathan Bubna says:

    I think it’s a poor idea to include the “remove()” method in your examples. HTMLElements have it in Chrome, but not in Firefox. This is why my fork of Voyeur adds its own remove() method (http://nbubna.github.io/HTML), as well as an only() that is more capable than eq() and an each() that is far, far more capable then use(). If you like Voyeur, check out HTML. 🙂

    1. Nathan Bubna says:

      likewise for innerText instead of textContent.

Leave a Reply

Your email address will not be published. Required fields are marked *

© 2016 Modern Web & our authors. All rights reserved.