DailyJS

Let's Make a Framework: NodeList, Collections and Arrays

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks lmaf dom NodeList

Let's Make a Framework: NodeList, Collections and Arrays

Posted by Alex R. Young on .
Featured

tutorials frameworks lmaf dom NodeList

Let's Make a Framework: NodeList, Collections and Arrays

Posted by Alex R. Young on .

Welcome to part 34 of Let's Make a Framework, the ongoing series about
building a JavaScript framework.

If you haven't been following along, these articles are tagged with
lmaf. The project we're creating is called Turing.

Last week I talked about using querySelectorAll. John-David
Dalton noticed that I missed part of the process -- converting the
resulting NodeList into an Array. This is an
important step which I'll discuss in this part.

NodeList and Collections

DOM Level 1 defines NodeList as an abstraction of ordered
collections of nodes. The specification is kept simple to avoid
constraining the underlying implementation. That means collections of
DOM elements can't be directly manipulated like an array, although it's
trivial to iterate over each element:

// Get the elements
var elements = document.querySelectorAll('p');

// Iterate over each element with a simple for loop
for (var i = 0; i < elements.length; i++) {
  console.log(elements[i]);
}

To see why NodeList doesn't do what we want, you'll notice
Array.prototype methods are missing:

document.querySelectorAll('p').push
// returns undefined

Ouch!

Another interesting point about NodeList is it's an
ordered collection. From DOM Level 1 Core:

getElementsByTagName Returns a NodeList of all descendant elements with a given tag name, in the order in which they would be encountered in a preorder traversal of the Element tree.

Over at the Mozilla
NodeList
documentation,
they have this to say:

This is a commonly used type which is a collection of nodes returned by getElementsByTagName, getElementsByTagNameNS, and Node.childNodes. The list is live, so changes to it internally or externally will cause the items they reference to be updated as well. Unlike NamedNodeMap, NodeList maintains a particular order (document order). The nodes in a NodeList are indexed starting with zero, similarly to JavaScript arrays, but a NodeList is not an array.

This isn't just what Mozilla implementations do, technically all
browsers should. This is from the specifications:

NodeLists and NamedNodeMaps in the DOM are "live", that is, changes to the underlying document structure are reflected in all relevant NodeLists and NamedNodeMaps.

Mozilla's documentation also points out a gotcha that I've run into
before:

Don't be tempted to use for...in or for each...in to enumerate the items in the list, since that will also enumerate the length and item properties of the NodeList and cause errors if your script assumes it only has to deal with element objects.

Converting NodeList into an Array

The simplest approach is probably the best:

function toArray(collection) {
  var results = [];
  for (var i = 0; i < collection.length; i++) {
    results.push(collection[i]);
  }
  return results;
}

The major downside of this is the results will no-longer be live --
the original NodeList is a reference to a set of objects rather than a
fixed result set.

In the Wild

jQuery has a method called makeArray:

makeArray: function( array, results ) {
  var ret = results || [];

  if ( array != null ) {
    // The window, strings (and functions) also have 'length'
    // The extra typeof function check is to prevent crashes
    // in Safari 2 (See: #3039)
    if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) {
      push.call( ret, array );
    } else {
      jQuery.merge( ret, array );
    }
  }

  return ret;
}

In this code, push refers to
Array.prototype.push.

When Prototype uses querySelectorAll, it wraps the output
in \$A() and uses .map(Element.extend) to make
each element a Prototype Element. This is similar to the
above, with the exception of Prototype extending each element.

Some other frameworks wrap the results in their own
NodeList class, rather than converting them to an array.

Implementation

The toArray function described above has been added to
turing.core.js in the form of turing.toArray and added to
turing.dom.get.

References