I was reading about document fragments and DOM reflow and wondered how document.createDocumentFragment differed from document.createElement as it looks like neither of them exist in the DOM until I append them to a DOM element.
I did a test (below) and all of them took exactly the same amount of time (about 95ms). At a guess this could possibly be due to there being no style applied to any of the elements, so no reflow maybe.
Anyway, based on the example below, why should I use createDocumentFragment instead of createElement when inserting into the DOM and whats the differnce between the two.
var htmz = "<ul>";
for (var i = 0; i < 2001; i++) {
htmz += '<li>link ' + i + '</li>';
}
htmz += '<ul>';
//createDocumentFragment
console.time('first');
var div = document.createElement("div");
div.innerHTML = htmz;
var fragment = document.createDocumentFragment();
while (div.firstChild) {
fragment.appendChild(div.firstChild);
}
$('#first').append(fragment);
console.timeEnd('first');
//createElement
console.time('second');
var span = document.createElement("span");
span.innerHTML = htmz;
$('#second').append(span);
console.timeEnd('second');
//jQuery
console.time('third');
$('#third').append(htmz);
console.timeEnd('third');
The difference is that a document fragment effectively disappears when you add it to the DOM. What happens is that all the child nodes of the document fragment are inserted at the location in the DOM where you insert the document fragment and the document fragment itself is not inserted. The fragment itself continues to exist but now has no children.
This allows you to insert multiple nodes into the DOM at the same time:
var frag = document.createDocumentFragment();
var textNode = frag.appendChild(document.createTextNode("Some text"));
var br = frag.appendChild(document.createElement("br"));
var body = document.body;
body.appendChild(frag);
alert(body.lastChild.tagName); // "BR"
alert(body.lastChild.previousSibling.data); // "Some text"
alert(frag.hasChildNodes()); // false
Another very important difference between creating an element and a document fragment:
When you create an element and append it to the DOM, the element is appended to the DOM, as well as the children.
With a document fragment, only the children are appended.
Take the case of:
var ul = document.getElementById("ul_test");
// First. add a document fragment:
(function() {
var frag = document.createDocumentFragment();
var li = document.createElement("li");
li.appendChild(document.createTextNode("Document Fragment"));
frag.appendChild(li);
ul.appendChild(frag);
console.log(2);
}());
(function() {
var div = document.createElement("div");
var li = document.createElement("li");
li.appendChild(document.createTextNode("Inside Div"));
div.appendChild(li);
ul.appendChild(div);
}());
Sample List:
<ul id="ul_test"></ul>
which results in this malformed HTML (whitespace added)
<ul id="ul_test">
<li>Document Fragment</li>
<div><li>Inside Div</li></div>
</ul>
You can think of a DocumentFragment as a virtual DOM. It's not connected to the DOM and unlike elements, it has no parent, EVER. You can then interact with the fragment as if it's a virtual document object. It's all in memory.
It's really helpful to use fragments when you have many DOM manipulations to make or style changes, because those will trigger reflows and repaints - expensive operations on the DOM that can slow the page load down.
The bonus you get with fragment is that it triggers only one reflow when the fragment is inserted into the DOM, no matter how many children it contains.
DocumentFragment is not an element or a Node. It's a stripped down Document object with a reduced set of properties and methods.
If you've ever heard of the virtual DOM with React, they are making heavy use of DocumentFragments in the ReactDOM library. That's why it's so performant.
Related
I want to query elements inside of my shadow dom slot element. Do I need to loop through what assignedNodes() returns and parse them myself?
I solved this issue by having the child element loop through the element's parent nodes until it finds the tag I am looking for.
var el = this; //the child element
while (el.parentNode) {
el = el.parentNode;
if (el.tagName == 'TAG-NAME'){ // The parent's tag name you're searching for.
this.parentElement = el;
break;
}
}
I also tried this with this.closest(), but Firefox had issues with that jumping between shadow dom.
I have this code which creates an element but the problem i am facing is it appends it at the very bottom of the page, in order for this to work i need it to be positioned in a certain place in the DOM how can i do this ?
var x = document.getElementById('options_10528_1');
var pos = document.getElementById('options-10528-list');
x.onclick = function(){
var elem = document.createElement('div');
elem.setAttribute("class","uk-warning");
elem.innerHTML = "Unfortunately, we cannot supply this medicine. Please consult your GP.";
document.body.appendChild(elem);
}
afterWhichNode.parentNode.insertBefore(newNode, afterWhichNode.nextSibling);
This code will insert a node after the afterwichNode, thats using vanilla javascript, if you are using jquery, just use .after()
Currently you are appending the element in the body tag, it will always goes at bottom. So if you want to append the element in a specific position, you have to append it in that container. let say you want to append it in "pos", you can do this:
pos.appendChild(elem);
I would like to use DocumentFragment and querySelector to make and modify DocumentFragments. I am using some code from Inserting arbitrary HTML into a DocumentFragment to create the fragments from HTML strings:
stringToDocumentFragment = function(html_string) {
var frag = document.createDocumentFragment();
var holder = document.createElement("div")
holder.innerHTML = html_string
frag.appendChild(holder)
return frag
}
And it works:
test_one = stringToDocumentFragment('<one><two>test</two></one>')
#document-fragment
test_one.querySelector('one')
> <one>...</one>
However, if I use elements like <html> or <body>, it fails:
test_two = stringToDocumentFragment('<html><body>test</body></html>')
#document-fragment
test_two.querySelector('html')
null
The behaviour is identical in both Chrome and Firefox.
A document fragment is meant to be a piece of a document, not an entire document - thus you should not have <body> and <html> in a document fragment. The point of a document fragment it to have a way to create or store a number of top level elements (a piece of some document). When inserted into an actual document, only the elements inside are inserted, not the top level container.
If you want an actual document that has html and body parts, then create a document, not a fragment.
If you just want to be able to use selector operations, then you don't need to use a fragement at all. Just create a div and set the innerHTML on a div and use querySelector operations on the div. You don't need a document fragment for that.
I'm just wondering if the following is possible, lets say we have a dom element and we want to wrap this element in a div. So a div is inserted between the element and it's parent. Then the div becomes the element's new parent.
But to complicate things, elsewhere we have already done things like:
var testElement = document.getElementByID('testID')
where testID is a child of the element to be warapped in a div. So after we have done our insertion will testElement still be valid?
BTW: I'm not using jquery.
Any ideas?
Thanks,
AJ
You can use replaceChild [docs]:
// `element` is the element you want to wrap
var parent = element.parentNode;
var wrapper = document.createElement('div');
// set the wrapper as child (instead of the element)
parent.replaceChild(wrapper, element);
// set element as child of wrapper
wrapper.appendChild(element);
As long as you are not using innerHTML (which destroys and creates elements), references to existing DOM elements are not changed.
Assuming you are doing your manipulation using standard DOM methods (and not innerHTML) then — yes.
Moving elements about does not break direct references to them.
(If you were using innerHTML, then you would be destroying the contents of the element you were setting that property on and then creating new content)
You probably want something like:
var oldParent = document.getElementById('foo');
var oldChild = document.getElementById('bar');
var wrapper = document.createElement('div');
oldParent.appendChild(wrapper);
wrapper.appendChild(oldChild);
In pure JS you can try something like this...
var wrapper = document.createElement('div');
var myDiv = document.getElementById('myDiv');
wrapper.appendChild(myDiv.cloneNode(true));
myDiv.parentNode.replaceChild(wrapper, myDiv);
Here is another example, only the new element wraps around 'all' of its child elements.
You can change this as necessary to have it wrap at different ranges. There isn't a lot of commentary on this specific topic, so hopefully it will be of help to everyone!
var newChildNodes = document.body.childNodes;
var newElement = document.createElement('div');
newElement.className = 'green_gradient';
newElement.id = 'content';
for (var i = 0; i < newChildNodes.length;i++) {
newElement.appendChild(newChildNodes.item(i));
newChildNodes.item(0).parentNode.insertBefore(newElement, newChildNodes.item(i));
}
You will want to modify the 'document.body' part of the newChildNodes variable to be whatever the parent of your new element will be. In this example, I chose to insert a wrapper div. You will also want to update the element type, and the id and className values.
I need to surround individual words inside an HTML element with SPAN tags. So something like this:
Foo <span class="f1 b0">bar <b>baz</b> boo blah</span> lorem ipsum
Should become this:
<span>Foo</span> <span class="f1 b0"><span>bar</span> <b><span>baz</span></b>
<span>blah</span></span> <span>lorem</span> <span>ipsum</span>
The reason is that I want to be able to figure out what word, specifically is under the cursor using "document.elementFromPoint(X, Y)". I tried using a simple regex:
theElement.innerHTML.replace(/\b(\w+)\b/g, "<span>$1</span>")
...but that won't work since the HTML element in question will most definitely have elements inside of it. I would just use that regex on the innerText instead of innerHTML but then I'd lose all existing formatting.
I have tried walking the children of the element, performing that regex replacement on each but sometimes the child elements have their own HTML tags within and I can't figure out how to perform a replacement of the text that comes before or after tags.
Anyone have a good solution?
To do this you will need to walk the DOM and understand how to process the individual nodes.
The basic walk code is this:
function walk(root)
{
if (root.nodeType == 3) // text node
{
doReplace(root);
return;
}
var children = root.childNodes;
for (var i = children.length - 1 ; i >= 0 ; i--)
{
walk(children[i]);
}
}
The walk function checks all the children of the input node and:
if it sees a text node it calls the replacement function
otherwise it recursively calls itself with the child node as the new input node.
Note that because the code in-place replaces nodes, the "children" node list will be affected by the replacement. To avoid this affecting the algorithm, the children are visited in reverse order.
The doReplace function is like this:
function doReplace(text)
{
var div = document.createElement("div");
div.innerHTML = text.nodeValue.replace(/\b(\w+)\b/g, "<span>$1</span>");
var parent = text.parentNode;
var children = div.childNodes;
for (var i = children.length - 1 ; i >= 0 ; i--)
{
parent.insertBefore(children[i], text.nextSibling);
}
parent.removeChild(text);
}
This creates a container node, then applies the regex and uses innerHTML to parse the result into a DOM fragment. The children of the div element can then replace the text node in the document. Again the moving of the nodes is done in reverse order so that the mutation of the source node list doesn't affect the loop.
Finally, the change can be applied by calling the walk function.
e.g.
window.onload = function() { walk(document.body); };
A complete working example can be found at http://www.alohci.net/text/html/wordwrapper.htm.ashx
You can inspect the source of this jQuery plugin to see how they do this, then extract the functionality you need.
try using .wrap() jQuery method