Create node list from a single node in JavaScript
Asked Answered
A

6

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.

Accusal answered 12/11, 2012 at 21:36 Comment(3)
possible duplicate of Creating a DOM NodeListLaunderette
Very similar... slightly different implementation. Good to have that reference here though.Accusal
Given the issue's with my answer, could you please un-accept my answer in order that I might delete it?Dredge
A
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
    };
}());
Assuasive answered 4/7, 2013 at 14:20 Comment(4)
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).Assuasive
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?Provincetown
@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 scopeAssuasive
Updated for ES6Assuasive
A
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;
Assuasive answered 13/11, 2012 at 15:25 Comment(10)
You're saying you can't necessarily access the element you want with any CSS selector?Assuasive
Yes. The node could have been dynamically created or selected from the document.Accusal
@RandyHall edit should work in almost any situation (with the exception of elm.nodeType !== 1)Assuasive
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.Accusal
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];};)Assuasive
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.Accusal
@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:Assuasive
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.Alleen
I liked the tag, fetch, and untag approach better, but used a data attribute to ensure no collision with anything else on the element: gist.github.com/GuyPaddock/93fbe863389a88fc9f2d495446c2598cQuyenr
@Paul Your approach is very smart!Shielashield
E
8

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

var nodelist = document.querySelectorAll("#mydiv");
Exudate answered 12/11, 2012 at 21:42 Comment(1)
Good thought as well, however that's not ALWAYS how I'm getting my node. +1 for idea other may find relevant and useful!Accusal
A
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
    };
}());
Assuasive answered 4/7, 2013 at 14:20 Comment(4)
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).Assuasive
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?Provincetown
@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 scopeAssuasive
Updated for ES6Assuasive
G
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;.

Gigot answered 6/9, 2017 at 11:59 Comment(4)
I'm not sure about the item declaration and +i syntax (never seen that before) but Reflect.construct() works well.Quyenr
I take it back... in Safari 12 we get "TypeError: Reflect.construct requires the third argument to be a constructor if present".Quyenr
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.Gigot
You don't even need to patch the prototype anymore!Dispatcher
R
3
var nodeList = document.createDocumentFragment();
nodeList.appendChild(document.getElementById("myDiv"));
Rayshell answered 12/11, 2012 at 21:44 Comment(4)
But nodeList instanceof NodeList returns false.Hough
You can return the nodeList after appending like such: console.log(nodeList.childNodes instanceof NodeList); // returns trueSmallish
The problem with this is that adding the node to the fragment removes it from the document DOM.Quyenr
This is handy for writing unit testsTreenware
B
0

Thanks @lao

var nodeList = document.createDocumentFragment();
nodeList.appendChild(document.getElementById("myDiv"));
nodeList = nodeList.childNodes

console.log(nodeList instanceof NodeList);
Bantling answered 23/6 at 17:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.