Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I convert an Array of nodes to a static NodeList?

NOTE: Before this question is assumed a duplicate, there is a section at the bottom of this question that addresses why a few similar questions do not provide the answer I am looking for.


We all know that it is easy to convert a NodeList to an Array and there are many ways to do it:

[].slice.call(someNodeList) // or Array.from(someNodeList) // etc... 

What I am after is the reverse; how can I convert an array of nodes into a static NodeList?


Why do I want to do this?

Without getting too deep into things, I am creating a new method to query elements on the page i.e:

Document.prototype.customQueryMethod = function (...args) {...} 

Trying to stay true to how querySelectorAll works, I want to return a static collection NodeList instead of an array.


I have approached the problem in three different ways so far:

Attempt 1:

Creating a Document Fragment

function createNodeList(arrayOfNodes) {     let fragment = document.createDocumentFragment();     arrayOfNodes.forEach((node) => {         fragment.appendChild(node);     });     return fragment.childNodes; } 

While this does return a NodeList, this does not work because calling appendChild removes the node from its current location in the DOM (where it should stay).

Another variation of this involves cloning the nodes and returning the clones. However, now you are returning the cloned nodes, which have no reference to the actual nodes in the DOM.


Attempt 2:

Attempting to "mock" the NodeList constructor

const FakeNodeList = (() => {      let fragment = document.createDocumentFragment();     fragment.appendChild(document.createComment('create a nodelist'));      function NodeList(nodes) {         let scope = this;         nodes.forEach((node, i) => {             scope[i] = node;         });     }      NodeList.prototype = ((proto) => {         function F() {         }          F.prototype = proto;         return new F();     })(fragment.childNodes);      NodeList.prototype.item = function item(idx) {         return this[idx] || null;     };      return NodeList; })(); 

And it would be used in the following manner:

let nodeList = new FakeNodeList(nodes);  // The following tests/uses all work nodeList instanceOf NodeList // true nodeList[0] // would return an element nodeList.item(0) // would return an element 

While this particular approach does not remove the elements from the DOM, it causes other errors, such as when converting it to an array:

let arr = [].slice.call(nodeList); // or let arr = Array.from(nodeList); 

Each of the above produces the following error: Uncaught TypeError: Illegal invocation

I am also trying to avoid "mimicking" a nodeList with a fake nodelist constructor as I believe that will likely have future unintended consequences.


Attempt 3:

Attaching a temporary attribute to elements to re-query them

function createNodeList(arrayOfNodes) {     arrayOfNodes.forEach((node) => {         node.setAttribute('QUERYME', '');     });     let nodeList = document.querySelectorAll('[QUERYME]');     arrayOfNodes.forEach((node) => {         node.removeAttribute('QUERYME');     });     return nodeList; } 

This was working well, until I discovered that it doesn't work for certain elements, like SVG's. It will not attach the attribute (although I did only test this in Chrome).


It seems this should be an easy thing to do, why can't I use the NodeList constructor to create a NodeList, and why can't I cast an array to a NodeList in a similar fashion that NodeLists are cast to arrays?

How can I convert an array of nodes to a NodeList, the right way?


Similar questions that have answers that don't work for me:

The following questions are similar to this one. Unfortunately, these questions/answers don't solve my particular problem for the following reasons.

How can I convert an Array of elements into a NodeList? The answer in this question uses a method that clones nodes. This will not work because I need to have access to the original nodes.

Create node list from a single node in JavaScript uses the document fragment approach (Attempt 1). The other answers try similar things at Attempts 2, and 3.

Creating a DOM NodeList is using E4X, and therefore does not apply. And even though it is using that, it still removes the elements from the DOM.

like image 886
KevBot Avatar asked Jul 18 '16 15:07

KevBot


People also ask

Is NodeList same as an array?

A NodeList may look like an array, but in reality, they both are two completely different things. A NodeList object is basically a collection of DOM nodes extracted from the HTML document. An array is a special data-type in JavaScript, that can store a collection of arbitrary elements.

Can you use array methods on NodeList?

A NodeList is an array-like object that represents a collection of DOM elements or more specifically nodes. It is just like an array, but you can not use the common array methods like map() , slice() , and filter() on a NodeList object.

Can you splice a NodeList?

Does it mean cannot apply splice function to NodeList array? Yes, it does, because a NodeList isn't an array, but an array-like object.

What is a NodeList explain a live NodeList and a static NodeList?

A NodeList object is a collection of nodes... The NodeList interface provides the abstraction of an ordered collection of nodes, without defining or constraining how this collection is implemented. NodeList objects in the DOM are live.


2 Answers

why can't I use the NodeList constructor to create a NodeList

Because the DOM specification for the NodeList interface does not specify the WebIDL [Constructor] attribute, so it cannot be created directly in user scripts.

why can't I cast an array to a NodeList in a similar fashion that NodeLists are cast to arrays?

This would certainly be a helpful function to have in your case, but no such function is specified to exist in the DOM specification. Thus, it is not possible to directly populate a NodeList from an array of Nodes.

While I seriously doubt you would call this "the right way" to go about things, one ugly solution is find CSS selectors that uniquely select your desired elements, and pass all of those paths into querySelectorAll as a comma-separated selector:

// find a CSS path that uniquely selects this element function buildIndexCSSPath(elem) {     var parent = elem.parentNode;       // if this is the root node, include its tag name the start of the string     if(parent == document) { return elem.tagName; }       // find this element's index as a child, and recursively ascend      return buildIndexCSSPath(parent) + " > :nth-child(" + (Array.prototype.indexOf.call(parent.children, elem)+1) + ")"; }  function toNodeList(list) {     // map all elements to CSS paths     var names = list.map(function(elem) { return buildIndexCSSPath(elem); });      // join all paths by commas     var superSelector = names.join(",");      // query with comma-joined mega-selector     return document.querySelectorAll(superSelector); }  toNodeList([elem1, elem2, ...]); 

This works by finding CSS strings to uniquely select each element, where each selector is of the form html > :nth-child(x) > :nth-child(y) > :nth-child(z) .... That is, each element can be understood to exist as a child of a child of a child (etc.) all the way up the root element. By finding the index of each child in the node's ancestor path, we can uniquely identify it.

Note that this will not preserve Text-type nodes, because querySelectorAll (and CSS paths in general) cannot select text nodes.

I have no idea if this will be sufficiently performant for your purposes, though.

like image 160
apsillers Avatar answered Sep 30 '22 16:09

apsillers


Here are my two cents:

  • Document is a native object and extending it may not be a good idea.
  • NodeList is a native object with a private constructor and no public methods to add elements, and there must be a reason for it.
  • Unless someone is able to provide a hack, there is no way to create and populate a NodeList without modifying the current document.
  • NodeList is like an Array, but having the item method that works just like using square brackets, with the exception of returning null instead of undefined when you are out of range. You can just return an array with the item method implemented:

myArray.item= function (e) { return this[e] || null; }

PS: Maybe you are taking the wrong approach and your custom query method could just wrap a document.querySelectorAll call that returns what you are looking for.

like image 39
Pablo Lozano Avatar answered Sep 30 '22 15:09

Pablo Lozano