DOM - Replace node with an array of nodes (efficiently) - javascript

What would be an efficient way to replace a DOM node with an array of nodes
(which are a simple array of detached nodes and not HTMLCollection)
(please no jQuery answers)
Demo page
HTML
<body>
<header>
<div>foo</div>
<div>bar</div>
</header>
</body>
JS
// generate simple dummy array of detached DOM nodes
var link1 = document.createElement('a');
link1.innerHTML = 'xxx';
var link2 = document.createElement('a');
link2.innerHTML = 'yyy';
var nodesArr = [link1, link2];
// get the element to replace at some place in the DOM.
// in the case, the second <div> inside the <header> element
var nodeToReplace = document.querySelectorAll('header > div')[1];
// replace "nodeToReplace" with "nodesArr"
for(let node of nodesArr)
nodeToReplace.parentNode.insertBefore(node, nodeToReplace);
nodeToReplace.parentNode.removeChild(nodeToReplace);

You can use a DocumentFragment instead of the array:
var nodesFragment = document.createDocumentFragment();
nodesFragment.appendChild(link1);
nodesFragment.appendChild(link2);
nodeToReplace.replaceWith(nodesFragment); // experimental, no good browser support
nodeToReplace.parentNode.replaceChild(nodesFragment, nodeToReplace); // standard
However, just inserting multiple elements in a loop shouldn't be much different with regard to performance. Building a document fragment from an existing array might even be slower.

My initial solution was a straightforward iteration:
// iterate on the Array and insert each element before the one to be removed
for(let node of nodesArr)
nodeToReplace.parentNode.insertBefore(node, nodeToReplace);
// remove the chosen element
nodeToReplace.parentNode.removeChild(nodeToReplace);

Related

How to get the DOM reference to an inserted document fragment

I am attempting to get the DOM reference of an inserted document fragment in vanilla Javascript. I'm currently using Node.appendChild() however the returned reference is the document fragment as outlined here: https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild#return_value
Is there any approach I could use to get the inserted DOM reference?
I did find the following Stack Overflow answer for a similar question but not related directly to document fragments, this solution uses either CSS animations or Mutation Observers but seems overkill for what I'm attempting. https://stackoverflow.com/a/38636244/13306195
const temp = document.createElement('template');
temp.innerHTML = '<span>Test</span>';
const neededReference = document.body.appendChild(temp.content);
The thing about document fragments is that you have to clone their content whenever appending them to the DOM in order to make them as DOM Nodes and not just the fragments.
const temp = document.createElement('template');
temp.innerHTML = '<span>Test</span>';
const neededReference =
document.body.appendChild(
temp.content.cloneNode(true).firstElementChild
);
console.log(neededReference); // should give the reference to span element
If the given child is a DocumentFragment, the entire contents of the DocumentFragment are moved into the child list of the specified parent node.
The return value of Node.appendChild() is the same thing with the document fragment which is empty.
I think you can put the children of document fragment into another container (e.g. an array). Those children are the real DOM refernces which are inserted.
const df = new DocumentFragment();
const t1 = document.createElement('template1');
t1.innerHTML = '<span>Test1</span>';
t1.id = "xxx"
const t2 = document.createElement('template2');
t2.innerHTML = '<span>Test2</span>';
df.appendChild(t1);
df.appendChild(t2);
document.body.appendChild(df);
window.alert(document.getElementById('xxx') === t1);

DOMParser parseFromString removing nodes only when iterating resulting body childNodes

I am writing an electronjs app. I want to parse a string to DOM nodes and try to use DOMParser parseFromString for that. Here is the code:
let str = '<div id="customerList" style="display: none;"><ul></ul></div><script type="text/javascript" src="../js/customerList.js"></script>';
let nodes = new DOMParser().parseFromString(str, 'text/html').body.childNodes;
console.log(nodes);
This returns a NodeList with 2 elements, the expected div and scriptl, in it. If I add the following part in the code, the first element, the div, disappears from the NodeList:
let str = '<div id="customerList" style="display: none;"><ul></ul></div><script type="text/javascript" src="../js/customerList.js"></script>';
let nodes = new DOMParser().parseFromString(str, 'text/html').body.childNodes;
console.log(nodes);
for (let node of nodes) {
contentDiv.appendChild(node);
}
The for loop is after the console.log and somehow alters the behavior of the code before. I can't seem to figure out, why the code behaves like it does though...Since I want to provide information about the first element in an ipcRenderer call, this is actually quite frustrating at the moment. Why does the code behave like it does?
Node.appendChild() moves a node to the new destination. That's why it disappears from your node list.
You can clone the node to avoid that like so:
let str = '<div id="customerList" style="display: none;"><ul></ul></div><script type="text/javascript" src="../js/customerList.js"></script>';
let nodes = new DOMParser().parseFromString(str, 'text/html').body.childNodes;
console.log(nodes);
for (let node of nodes) {
contentDiv.appendChild(node.cloneNode());
}
This will append clones of all(!) nodes from the list and keep your nodes list as is.
Reference: https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild

Does document.createElement keep triggering DOMContentLoaded event listener?

When I run this script, the page is continuously loading and eventually freezes. Is this because everytime I create an element, the main DOMContentLoaded listener is being called?
If so, how can I stop this recursive behaviour and just add one node to every pre existing node?
//Waits for page to load
document.addEventListener('DOMContentLoaded', function() {
//Get all elements
var items = document.getElementsByTagName("*");
//Loop through entire DOM
for (var i = 0; i < items.length; i++) {
//If it is not a text node
if (!(items[i].nodeType == 3)){
//Create a div
var newDiv = document.createElement("div");
//Add div to current object
items[i].appendChild(newDiv);
}
}
});
It's because items is referencing a "live list". This means that any updates to the DOM are going to be reflected in your list if they match the original selector.
Because you're appending a div, and your selector selects all elements, it gets added to the list, pushing any subsequent members up an index, and so the iteration continues.
To avoid this, make a non-live copy of the collection before iterating.
var items = Array.from(document.getElementsByTagName("*"));
And FYI, the if (!(items[i].nodeType == 3)){ can be removed because getElementsByTagName will never return text nodes.
If you're supporting very old versions of IE, you may want to check that the .nodeType === 1, since some of those old versions included comment nodes when using "*".
Lastly, you can use modern features to clean this up a bit.
document.addEventListener('DOMContentLoaded', () => {
for (const el of [...document.getElementsByTagName("*")]) {
var newDiv = el.appendChild(document.createElement("div"));
// Work with newDiv
}
});
You get a continuously loading page because you have this:
var items = document.getElementsByTagName("*");
Which returns a "live" node list - meaning a list that can/will change as the matched elements change. Since your selector is for everything and then you create new elements, the set of matched elements changes (it gets bigger). Which, in turn causes the length of the node list to change, which keeps your loop running.
You should not try to match all the elements or you should use another method for getting the elements that doesn't return a live node list, like querySelectoAll()

How can I hold the selected element's node by value (and not reference) in Javascript?

I had to remove everything in the body except for images in the DOM.
So, this is what I did.
Stored the img tags in the variable.
var img_nodes = document.getElementsByTagName('img');
Removed every thing inside the body
var body_node = document.getElementsByTagName('body');
body_node[0].innerHTML = ''
Added the stored img nodes as children
for each (child in img_nodes){
body.appendChild(child);
}
But after I had run body_node[0].innerHTML = '', the img_nodes became undefined.
I guess that happened because element node was being stored in list as objects (as a reference). How can I save my selected img_nodes objects ?
You could clone the image nodes before clearing the body contents. See the documentation on cloneNode. Example:
var cloned_nodes = [];
for each (child in img_nodes){
cloned_nodes.push(child.cloneNode(true));
}

Trouble Replacing Multiple Links With iFrame Via Javascript

I'm trying to parse a page with javascript to replace links belonging to a specific class with an iframe to open a corresponding wikipedia page [so that rather than having a link you have an embedded result]. The function detects links properly but something about the replaceChild() action causes it to skip the next instance... as if it does the first replace and then thinks the next link is the one it just worked on, probably as a result of the loop.
For example, if there's 2 links on the page, the first will parse and the second will not even be seen but if there's 3, the first two will be parsed using the attributes from the first and third.
Can anyone suggest an alternative way of looping through the links that doesn't rely on a count function? Perhaps adding them to an array?
Sample Links
wiki it
Sample Javascript
(function(){
var lnks = document.getElementsByTagName("a");
for (var i = 0; i < lnks.length; i++) {
lnk = lnks[i]; if(lnk.className == "myspeciallinks"){
newif=document.createElement("iframe");
newif.setAttribute("src",'http://www.wikipedia.com');
newif.style.width="500px";
newif.style.height="100px";
newif.style.border="none";
newif.setAttribute("allowtransparency","true");
lnk.parentNode.replaceChild(newif,lnk);
}
}
})();
The problem here is that document.getElementsByTagName returns a NodeList and not an array. A NodeList is still connected to the actual DOM, you cannot safely iterate over its entries and at the same time remove the entries from the DOM (as you do when you replace the links).
You will need to convert the NodeList into an array and use the array for iteration:
(function(){
var lnksNodeList = document.getElementsByTagName("a");
// create an array from the above NodeList and use for iteration:
var lnks = Array.prototype.slice.call(lnksNodeList);
for (var i = 0; i < lnks.length; i++) {
var lnk = lnks[i];
if (lnk.className == "myspeciallinks") {
var newif = document.createElement("iframe");
newif.setAttribute("src", 'http://www.wikipedia.com');
newif.style.width = "500px";
newif.style.height = "100px";
newif.style.border = "none";
newif.setAttribute("allowtransparency", "true");
lnk.parentNode.replaceChild(newif, lnk);
}
}
})();
According to the MDN documentation:
Returns a list of elements with the given tag name. The subtree underneath the specified element is searched, excluding the element itself. The returned list is live, meaning that it updates itself with the DOM tree automatically. Consequently, there is no need to call several times element.getElementsByTagName with the same element and arguments.
Therefore, the collection shrinks every time you replace an a. You could change your loop to decrement i whenever you do a replace.

Categories