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)
Related
I am making a "psuedo", "super" form like custom-element. I want to traverse the nodes in order looking for nodes that match a particular criteria (one is that they have a method function called validate, the other is that they have the properties name and value). In my traversal I want to
Avoid child nodes of an element which have transferred to slots within an elements shadowroot.
Traverse the shadow root on an element and in that traversal pick up elements that have been transferred to the slots within it.
Not traverse the children or the shadow root of elements that match the criteria I mention above.
It seems that there are two possible approaches.
Create a "treewalker" and use that to iterate through the nodes it finds.
Simply use the Node API with such calls as Node.hasChildNodes() and Node.childNodes
However, I can find no documentation on either of these two possibilities that explains what happens to slotted content. The documentation on these typically ignores any mention of slots and slotted content - probably because they were written before slots became part of the spec.
There is obviously Slot.assignedNodes() method which I could use to find whats in a slot as I am traversing the shadowRoot (although even the documentation of that is ambiguous in what the optional flatten value does - it says it returns default content when set to true, but does it return default content if there is no actual slots provided when the value is set to false? - and why is it called "flatten" anyway, doesn't seem appropriate for what it does?). But if I am using that, I also need a way to avoid the nodes which have been moved to these slots when traversing the children
So how is the best way of forming the actions described above can someone give me some clues?
I eventually found the best way of doing this was manually. The crux of the test is that node.assignedSlot is not null when an element is assigned to a slot, so can use this when scanning children to ignore children when they are assigned to a slot (and to include them when scanning slot.assignedNodes()
Here is a module with a walk default function to walk the element provided. criteria is a callback function per node which the caller can use to check if the node meets the criteria looked for (see question) and therefore not scan the children.
function _walk(node,criteria, slot) {
if (node.assignedSlot === null || node.assignedSlot === slot) {
if (node.localName === 'slot') {
const assignedNodes = node.assignedNodes();
if (assignedNodes.length === 0) {
_walkA(node.children, criteria);
} else {
_walkA(assignedNodes.filter(n => n.nodeType === Node.ELEMENT_NODE), criteria, node);
}
} else if (!criteria(node)) {
if (customElements.get(node.localName)) _walkA(node.shadowRoot.children,criteria);
_walkA(node.children, criteria);
}
}
}
function _walkA(nodes,criteria, slot) {
for (let n of nodes) {
_walk(n,criteria, slot);
}
}
export default function walk(walknode, criteria) {
_walk(walknode,criteria,null);
}
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.
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.
Fallback is irrelevant. No libraries, please.
We have an dom object reference, we'll call obj. It's actually an event.target.
We have a node list, we'll call nodes, which we've gotten with querySelectorAll and a variable selector.
nodes may have 1 or many elements, and each each of those elements may have children.
We need to determine if obj is one of those node elements, or children elements of those node elements. We're looking for "native" browser functionality here, we can totes write our own for loop and accomplish this, we are looking for alternatives.
Something like:
nodes.contains(obj) OR nodes.indexof(obj)
Solutions involving other methods of retrieving the node list to match against are acceptable, but I have no idea what those could be.
If <=IE11 is not a concern then I think the cleanest is to use Array.from
Array.from(nodes).find(node => node.isEqualNode(nodeToFind));
I'm not sure if this will search beyond the first level of the NodeList, but you can use this expression recursively to traverse it and check if the element 'obj' is in the NodeList 'nodes'.
[].indexOf.call(nodes, obj)
I did something like this:
Array.prototype.find.call(style.childNodes, function(child) {
if(child.textContent.includes(drawer.id)) {
console.log(child);
}
});
Seems to work. Then child is another html node, which you can manipulate however you like.
I don't think there's a built-in DOM method for that. You'd need to recursively traverse your NodeList, and check for equality with your element. Another option is to use Element.querySelectorAll on each first-level elements from your NodeList (looking for your element's id, for example). I'm not sure how (inn)efficient that would be, though.
[...nodes].includes(targetNode)
In addition to Dominic's answer as function:
function nodelist_contains (nodelist, obj)
{
if (-1 < Array.from (nodelist).indexOf (obj))
return true;
return false;
}
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();
}
};