18

This is one of those it seems so simple, but I cannot come up with a good way to go about it.

I have a node, maybe nodelist = document.getElementById("mydiv"); - I need to normalize this to a node list. And not an array either: an actual, bona-fide nodeList object.

Not nodelist = [document.getElementById("mydiv")];

No libraries, please.

Ruben-J
  • 2,663
  • 15
  • 33
Randy Hall
  • 7,716
  • 16
  • 73
  • 151

5 Answers5

10

Take any element already referenced in JavaScript, give it an attribute we can find using a selector, find it as a list, remove the attribute, return the list.

function toNodeList(elm){
    var list;
    elm.setAttribute('wrapNodeList','');
    list = document.querySelectorAll('[wrapNodeList]');
    elm.removeAttribute('wrapNodeList');
    return list;
}

Extended from bfavaretto's answer.


function toNodeList(elm, context){
    var list, df;
    context = context // context provided
           || elm.parentNode; // element's parent
    if(!context && elm.ownerDocument){ // is part of a document
        if(elm === elm.ownerDocument.documentElement || elm.ownerDocument.constructor.name === 'DocumentFragment'){ // is <html> or in a fragment
            context = elm.ownerDocument;
        }
    }
    if(!context){ // still no context? do David Thomas' method
        df = document.createDocumentFragment();
        df.appendChild(elm);
        list = df.childNodes;
        // df.removeChild(elm); // NodeList is live, removeChild empties it
        return list;
    }
    // selector method
    elm.setAttribute('wrapNodeList','');
    list = context.querySelectorAll('[wrapNodeList]');
    elm.removeAttribute('wrapNodeList');
    return list;
}

There is another way to do this I thought of recently

var _NodeList = (function () {
    var fragment = document.createDocumentFragment();
    fragment.appendChild(document.createComment('node shadows me'));
    function NodeList (node) {
        this[0] = node;
    };
    NodeList.prototype = (function (proto) {
        function F() {} // Object.create shim
        F.prototype = proto;
        return new F();
    }(fragment.childNodes));
    NodeList.prototype.item = function item(i) {
        return this[+i || 0];
    };
    return NodeList;
}());

Now

var list = new _NodeList(document.body); // note **new**
list.constructor === NodeList; // all these are true
list instanceof NodeList;
list.length === 1;
list[0] === document.body;
list.item(0) === document.body;
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • You're saying you can't necessarily access the element you want with any CSS selector? – Paul S. Nov 13 '12 at 15:33
  • Yes. The node could have been dynamically created or selected from the document. – Randy Hall Nov 13 '12 at 15:39
  • @RandyHall edit should work in almost any situation (with the exception of `elm.nodeType !== 1`) – Paul S. Nov 13 '12 at 15:56
  • So basically, if it's in the document or a context container, do your method, if it's not, David Thomas' method would be safe. Nice. – Randy Hall Nov 13 '12 at 16:33
  • I don't know why you're unhappy with an Array, though. Other than as a mental challenge. If you don't care how the NodeList is created then it's "live-ness" isn't of much importance, and nearly all methods and properties are the same as those on arrays. (You can add _item_ with `Array.prototype.item = function(i){return this[i];};`) – Paul S. Nov 13 '12 at 17:23
  • 1
    In most cases, you'd be completely correct. But there's functions written by others that I have to utilize but cannot affect that require and check for a nodelist specifically. – Randy Hall Nov 13 '12 at 17:29
  • @RandyHall I recently had some inspiration about this. I'm not sure how the check was done, but I'm pretty sure this will pass it. **edit** oh, I see I posted another answer to do the same that's further down, lol.. well at least the way I wrote it this time works in older browsers and has a little less to do each construction D: – Paul S. Mar 30 '14 at 12:21
  • Not working on Firefox and Chrome. 3rd condition is raising `Invalid Invocation` error on Chrome. 4th and 5th conditions are evaluating to `false` on Firefox, but `true` on Chrome. – Joyce Babu Nov 17 '15 at 06:42
  • I liked the tag, fetch, and untag approach better, but used a data attribute to ensure no collision with anything else on the element: https://gist.github.com/GuyPaddock/93fbe863389a88fc9f2d495446c2598c – GuyPaddock Feb 29 '20 at 01:39
  • @Paul Your approach is very smart! – doraemon May 20 '23 at 11:43
8

If you're targeting browsers that support document.querySelectorAll, it will always return a NodeList. So:

var nodelist = document.querySelectorAll("#mydiv");
bfavaretto
  • 71,580
  • 16
  • 111
  • 150
  • Good thought as well, however that's not ALWAYS how I'm getting my node. +1 for idea other may find relevant and useful! – Randy Hall Nov 12 '12 at 21:43
7

Reviving this because I recently remembered something about JavaScript. This depends on how the NodeList is being checked, but..

const singleNode = ((nodeList) => (node) => {
  const layer = { // define our specific case
    0: { value: node, enumerable: true },
    length: { value: 1 },
    item: {
      value(i) {
        return this[+i || 0];
      }, 
      enumerable: true,
    },
  };
  return Object.create(nodeList, layer); // put our case on top of true NodeList
})(document.createDocumentFragment().childNodes); // scope a true NodeList

Now, if you do

const list = singleNode(document.body); // for example

list instanceof NodeList; // true
list.constructor === NodeList; // true

and list has properties length 1 and 0 as your node, as well as anything inherited from NodeList.

If you can't use Object.create, you could do the same except as a constructor with prototype nodelist and set this['0'] = node;, this['length'] = 1; and create with new.


ES5 version

var singleNode = (function () {
    // make an empty node list to inherit from
    var nodelist = document.createDocumentFragment().childNodes;
    // return a function to create object formed as desired
    return function (node) {
        return Object.create(nodelist, {
            '0': {value: node, enumerable: true},
            'length': {value: 1},
            'item': {
                "value": function (i) {
                    return this[+i || 0];
                }, 
                enumerable: true
            }
        }); // return an object pretending to be a NodeList
    };
}());
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • Also going to point out that if you want to use `list.item`, you'll have to shadow it to avoid an illegal invocation (perhaps use two-level prototyping). – Paul S. Jul 04 '13 at 15:16
  • I appreciate that it's been several years, by why does `singleNode` return a self-executing function? It seems to work fine by just returning the `Object.create...` after creating the empty `DocumentFragment`? Is it just so the document fragment is created once, no matter how many times it's called? – Regular Jo Jun 09 '21 at 19:55
  • 1
    @RegularJo yep, as we're putting the document fragment in the prototype and not modifying it we can re-use the same fragment as many times as we'd like if we capture it in a scope – Paul S. Jun 11 '21 at 11:28
  • Updated for ES6 – Paul S. Jun 11 '21 at 11:40
7

Yet another way to do this based on Reflect.construct: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct

As in other cases it requires patching NodeList.prototype.item to make calls to this function work.

NodeList.prototype.item = function item(i) {
    return this[+i || 0];
};
let nl = Reflect.construct(Array, [], NodeList);

To create it with nodes pass array of nodes as second argument. This method passes checks:

list instanceof NodeList; // true
list.constructor === NodeList; // true

Array, created with it, is iterable with for..of, forEach and other standard methods and you can add elements into it with simple nl[n] = node;.

Lain inVerse
  • 81
  • 1
  • 4
  • I'm not sure about the item declaration and `+i` syntax (never seen that before) but Reflect.construct() works well. – GuyPaddock Jan 31 '20 at 16:24
  • I take it back... in Safari 12 we get "TypeError: Reflect.construct requires the third argument to be a constructor if present". – GuyPaddock Feb 29 '20 at 00:51
  • Well, you can try to play with this then: div.parentNode.querySelectorAll(`:scope > :nth-child(${Array.from(div.parentNode.children).indexOf(div)+1})`) Not sure about compatibility with Safari 12 and it won't work in IE11 for sure, but it returns actual NodeList. Element must have a parent, though. Could be solved by temporarily adding it into another element in case parentNode is null. – Lain inVerse Mar 01 '20 at 01:13
  • You don't even need to patch the prototype anymore! – TheBrenny Aug 25 '21 at 03:50
3
var nodeList = document.createDocumentFragment();
nodeList.appendChild(document.getElementById("myDiv"));
Orbiting Eden
  • 1,522
  • 13
  • 16