retain reference to appended node javascript - javascript

No libraries please. Beyond my control.
I'm appending a document fragment to the dom. THIS ALL WORKS. No problemo.
How do I retain/retrieve the node list after the fragment is appended? Relevant code:
var frag = document.createDocumentFragment();
//add some content to the fragment
element.appendChild(frag);
Again, this works! I do NOT need a troubleshoot on how to add things to the dom!
If I set var e = element.appendChild(frag);, everything gets appended normally, but e = an empty document fragment.
I'm looking for some smooth magic voodoo here. Don't send me looping over the entire dom. The content could be anything, one or many nodes with or without children. If there's some trick with querySelectorAll or something that would be acceptable.
Thanks!
EDIT
Upon further poking, it appears that e above is in fact a returned reference to the frag var, that is empty after appending it to the dom. It's much like the elements were neatly slid off of the fragment into the dom, and the fragment just lays around empty.

It's exactly what you've described; when the elements are appended to an element, they're removed from the fragment to prevent memory leaks from lingering references.
One way to get those child nodes is to make a shallow copy of the fragment's childNodes before doing appendChild():
// make shallow copy, use Array.prototype to get .slice() goodness
var children = [].slice.call(frag.childNodes, 0);
element.appendChild(frag);
return children;

If you're looking just after appending it, the lastChild is the way to go. Use it like this:
var e = element.lastChild;
More info in SitePoint

From doc of DocumentFragment:
Various other methods can take a document fragment as an argument (e.g., any Node interface methods such as appendChild and insertBefore), in which case the children of the fragment are appended or inserted, not the fragment itself.
So appendChild method return the documentFragment but it's empty because its child nodes are inserted in your element object and (from appendChild doc):
[...] a node can't be in two points of the document simultaneously. So if the node already has a parent, it is first removed, then appended at the new position.
Now...
You can store in a cache array your children and after return it (or see Jack solution :-))

Like Jack's solution, but if you are using esnext you could use Array.from as
const children = Array.from(frag.children);
element.appendChild(frag);
return children;

If you have single main element in your document fragment, you can use .firstElementChild and that's all. To make multiple copies use .cloneNode(true).
Example - https://codepen.io/SergeiMinaev/pen/abLzQjZ

Thanks to #The Sloth's comment, I found this solution, which finally solved it for me:
var node = element.lastElementChild
In my case, element.lastChild returned a text node instead of the appended node.

Related

insertAdjacentElement not accepting document fragment

I am trying to execute below snippet
let frag = document.createDocumentFragment();
let divElement = document.createElement('div');
divElement.insertAdjacentElement('afterend', frag);
I am getting the below error.
Uncaught TypeError: Failed to execute 'insertAdjacentElement' on 'Element': parameter 2 is not of type 'Element'.
What is the restriction here? reference about error details would be much appreciated.
Please suggest an alternative to do it efficiently.
Fragments can be composed of multiple elements, not just single elements. In such a case, if the technique in your question worked, insertAdjacentElement would be inserting multiple elements, not just a single element. (It's not called insertAdjacentElements)
It just isn't allowed. Unlike appendChild, insertAdjacentElement can't take a fragment as an argument.
You could iterate through all children of the fragment and insert them afterend:
// Create fragment, append elements
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createElement('span')).textContent = 'span1';
fragment.appendChild(document.createElement('span')).textContent = 'span2';
fragment.appendChild(document.createElement('span')).textContent = 'span3';
// First cast fragment.children from an HTMLCollection to an Array
// Since insertAdjacentElement will remove the element from fragment.children
// It will mutate on each loop causing issue
const childrenAsArray = Array.from(fragment.children);
// Iterate through fragment
for (const element of childrenAsArray ) {
divElement.insertAdjacentElement('beforeend', element);
}
<div id="divElement"></div>
Or you could call insertAdjacentElement directly instead of putting the elements into a fragment first.
createDocumentFragment is never part of main dom.In dom tree document fragment is replaced by its child. Also it cannot inherit from element base class. So it is not a valid element too. The syntax of insertAdjacentElement is targetElement.insertAdjacentElement(position, element) where element need to be a valid one as described by the above link.
Hence this error

appendChild() strips content from imported template

I obtain an active copy of an HTML5 <template> using function importNode():
function getTemplate() {
var t = document.getElementById("example");
return document.importNode(t.content,true);
}
After this, I fill the dynamic data,
var t = fillTemplate({
id:"test",
text:"Enter test data"
});
and finally, I append the node into the target container:
var c = document.getElementById("container");
var result = c.appendChild(t);
My problem: the result node has all its content stripped off: I can't access the component elements of the template in the result node. Actually, the result node contains no child nodes at all once the appendChild operation has been performed.
I expect that the return value of appendChild should point to the node that has been inserted into the container and which is now part of the active document. Any explanation why this is not the case?
Here is the jsfiddle (tested in Chrome 53):
https://jsfiddle.net/rplantiko/mv2rbhym/
It is due to the fact that you don't manipulate a Node but a DocumentFragment.
If you want get the number of Nodes inserted, you should perform the call on the parent container (c in your example) then you'll get the right answer (5).
But if you want to count only the child elements you added, you should not use childNodes, but the property of the ParentNode interface children:
c.childNodes.length // = 5
c.children.length // = 2
After being appended to the container c, the DocumentFragment t has no children any more.
From the MDN documentation:
Various other methods can take a document fragment as an argument
(e.g., any Node interface methods such as Node.appendChild and
Node.insertBefore), in which case the children of the fragment are
appended or inserted at the location in the DOM where you insert the
document fragment, not the fragment itself. The fragment itself
continues to exist (in memory) but now has no children.
The DOM 3 W3C recommendation is also clear:
Furthermore, various operations -- such as inserting nodes as children
of another Node -- may take DocumentFragment objects as arguments;
this results in all the child nodes of the DocumentFragment being
moved to the child list of this node.

Iterate over all node objects not only element objects

I've read the following very good article:
Difference between Node object and Element object?
as to clear out the difference between a node object and an element object. I 've understood that
element objects are a subset of node objects.
So, after that I've come upon the following query: In which way may I iterate through all the node objects? By using document.getElementsByTagName('*') ,I think I am getting all the element objects since all of them have value 1 for their .nodeType property. Am I right and if yes, how may I include into my results all these nodes that are not elements?
Thank you
I don't believe there's any standard DOM function that will return every Node (as opposed to Element) in the entire document.
You would probably have to use recursive DOM traversal to find them all with something like this:
function getAllNodes(parent, nodes) {
parent = parent || document;
nodes = nodes || [];
var child = parent.firstChild;
while (child) {
nodes.push(child);
if (child.hasChildNodes) {
getAllNodes(child, nodes);
}
child = child.nextSibling;
}
return nodes;
}
As written you can just write var nodes = getAllNodes() and it'll automatically start at the document root.
The answer is in your question. You use document.getElementsByTagName. Let me repeat that for you. getElementsByTagName.
Non-element nodes don't have a tag name, an identifier, or anything like that. The only way to designate them is by where they are in the document structure.
You can get a reference to an Element and then browse its childNodes, as has been suggested already.
Another way is to use an XPath, which, unlike CSS selectors, is able to point directly to any Node, including text and comment Nodes.
document.evaluate("*", document.documentElement, null, XPathResult. UNORDERED_NODE_ITERATOR_TYPE)

How to build a cache of the DOM that does not change

So I tried to build a cache of the DOM:
var DOM = document.getElementsByTagName('*');
However, the DOM variable seems to be a dynamic reference, so that if I change an element in the DOM, the DOM variable changes as well.
I tried iterating through the DOM variable and using the cloneNode method to create a deep copy of each node. This works in that it does not change when I change the DOM. However, the problem is that a cloned node does not equal its original DOM node when you compare them with the === operator.
So to sum up, I'm looking to create a cache of the DOM that does not change but whose nodes are still equal to the original DOM nodes.
document.getElementsByTagName returns a "live" NodeList, which isn't what you think at all. When you access the list, the DOM is traversed (implementation may cache it) every time to get the result. This gives the illusion of the list being live.
document.getElementsByTagName("div") === document.getElementsByTagName("div")
//true
To do what you want, simply convert it to an array. DOM = [].slice.call(DOM)
You seem open to a jQuery solution, so:
$("*")
will return a jQuery object containing all the elements. It will not be updated as the DOM changes.
Or if you just want elements within the <body> (i.e., not <script> or <meta> elements, etc., from the <head>) then:
$("body *")
Being a jQuery object it will of course allow you to access jQuery methods, but you can also access the DOM elements directly with array notation:
var DOM = $("body *");
DOM.show(); // example jQuery method call
alert(DOM.length); // show count of elements in DOM
alert(DOM[4].value) // example of direct access to fifth DOM element
I prefer to use the following methodology:
https://gist.github.com/3841424#file-domcache-js
Or, you may replace the DOM object with a method in this implementation:
var myNS = {
myEventHandler: function(event){
this.DOM.$el.doSomething();
},
cacheDOM: function(){
return {
$el: $("#matrix")
};
},
initialize: function(){
this.DOM = this.cacheDOM();
}
};

Use getElementById for elements that are not [yet] in the DOM?

As far as I know document.getElementById('myId') will only look for HTML elements that are already in the document. Let's say I've created a new element via JS, but that I haven't appended it yet to the document body, is there's a way I can access this element by its id like I would normally do with getElementById?
var newElement = document.createElement('div');
newElement.id = 'myId';
// Without doing: document.body.appendChild(newElement);
var elmt = document.getElementById('myId'); // won't work
Is there a workaround for that?
(I must tell that I don't want to store any reference to this particular element, that's why I need to access it via its Id)
Thank you!
If it isn't part of the document, then you can't grab it using document.getElementById. getElementById does a DOM lookup, so the element must be in the tree to be found. If you create a floating DOM element, it merely exists in memory, and isn't accessible from the DOM. It has to be added to the DOM to be visible.
If you need to reference the element later, simply pass the reference to another function--all objects in JavaScript are passed by reference, so working on that floating DOM element from within another function modifies the original, not a copy.
For anyone stumbling upon this issue in or after 2019, here is an updated answer.
The accepted answer from Andrew Noyes is correct in that document.getElementById won't work unless the element exists in the document and the above code already contains a reference to the desired element anyway.
However, if you can't otherwise retrieve a direct reference to your desired element, consider using selectors. Selectors allow you to retrieve nodes that aren't necessarily in the DOM by using their relationship to other nodes, for example:
var child = document.createElement("div");
child.id = "my_id";
var parent = document.createElement("div");
parent.appendChild(child);
var child2 = parent.querySelector("#my_id");
getElementById is a method on the document object. It's not going to return anything not in the document.
On not storing a reference, huh? If you could magically pull it out of the air by id, then the air would be a reference to it.
If you've created it, just pass the object to other functions and access it directly?
function createDiv()
{
var newElement = document.createElement('div');
doWorkWithDiv(newElement);
}
function doWorkWithDiv(element)
{
element.className = 'newElementCSS';
element.innerHTML = 'Text inside newElement';
addToDoc(element);
}
function addToDoc(element)
{
document.body.appendChild(element);
}

Categories