Can anyone please explain briefly what documentFragment actually does? I have been searching for a clear explanation but I don't get any until now.
what I read is, documentFragment is something like DOM like structure where we can add modify DOM elements without interrupting the actual flow of the document.
I also read, documentFragment is faster than appending each element into DOM one by one. It felt to me like, documentFragment does not recalculate styles every time so it is faster.
I have two examples,
DOING IT IN FRAGMENT WAY:
var frag = document.createDocumentFragment();
var div1 = document.createElement("div");
var div2 = document.createElement("div");
frag.appendChild(div1);
frag.appendChild(div2);
document.getElementById("someId").appendChild(frag);
DOING IT IN NORMAL WAY:
var div = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");
div.appendChild(div1);
div.appendChild(div2);
document.getElementById("someId").appendChild(div);
what actually happens in the above two examples?
There's an important difference between "the fragment way" and "the normal way":
Using document.createElement:
const div = document.createElement('div');
div.appendChild(document.createTextNode('Hello'));
div.appendChild(document.createElement('span'));
document.body.appendChild(div);
console.log(div.childNodes); // logs a NodeList [#text 'Hello', <span>]
This results in the following DOM structure:
<body>
<div>
Hello
<span></span>
</div>
</body>
Using DocumentFragment:
const frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode('Hello'));
frag.appendChild(document.createElement('span'));
document.body.appendChild(frag);
console.log(frag.childNodes); // logs an empty NodeList
This results in the following DOM structure:
<body>
Hello
<span></span>
</body>
That means that calling appendChild or insertBefore with an instance of DocumentFragment moves the child nodes of the document fragment to the new parent node. After that, the document fragment is empty.
As you have correctly mentioned, it can be more efficient to create a document fragment and append multiple elements to it than to append those elements to the real DOM one by one, causing the browser to re–render parts of the page every time. Because the contents of the document fragment are not visible on screen, the page has to be re–rendered only once.
Whenever you create a large DOM structure, it can be advisable to create it within a document fragment and append that to the DOM when you're done.
For appending only 2 childs you will not see any performance issue. Imagine that you have an array of books that includes 100 items and you want to append them to the DOM. You would write this code:
let books=[,,,,,,,,,,,,,,,,]
let bookList;
document.addEventListener('DOMContentLoaded',load)
function load(){
bookList=document.getElementById('books')
books.forEach(book=>{
let li=document.createElement('li')
li.textContext=book
li.class="bookItem"
// here is the headache part
bookList.appendChild(li)
}))
}
So you are going to loop through 100 times and each time, you are going to tell the browser that redraw the screen. This will take up a lot of resources. Depending on the system that you are using, you might see the screen is flickering.
with fragment I would write load like this:
function load(){
bookList=document.getElementById('books')
let df=new DocumentFragment()
books.forEach(book=>{
let li=document.createElement('li')
li.textContext=book
li.class="bookItem"
// we are appending to df not to the bookList
df.appendChild(li)
})
// I collected all list elements inside DocumentFragment
// Now I append it to the bookList
bookList.appendChild(df)
}
creating document fragment is like creating a div. But it does not add any Html to the page. You are not going to see in HTML source code:
<DocumentFragment></DocumentFragment>
It is just an empty container that is used to hold other parts of Html. a fragment can be injected and cloned in a single operation instead of having to inject and clone each
individual node over and over again. We are achieving exact same thing with much better performance.
Related
I want to create an HTML element (a div) using javascript to use that as a tooltip.
I can create the simple element by doing:
const element = document.createElement("div");
element.id = "myID"
and that works fine...however, I want to add a table (and some other HTML) inside the tooltip, so I was trying to do
element.appendChild(childElement); //where the childElement is another document.createElement("") that contains all the HTML I want.
or:
element.insertAdjacentHTML('afterbegin', '<table></table>');
however, nothing happens. there's no error, but it won't append it either. Am I missing something?
If it matters, this is happening inside the contentScripts.js of a chrome extension I'm building.
EDIT
Full code of div creation:
const element = document.createElement("div");
element.id = "tooltip-creation";
element.classList.add("tooltip");
const childElement = document.createElement("div");
childElement.id = "data";
//I've attempted to do .appendChild, innerHTML, and .insertAdjacentHTML (as can be seen here) and neither works but no error is given.
element.appendChild(childElement);
element.insertAdjacentHTML('afterbegin','<table border="1"><tr><td><strong>OMG</strong></td></tr></table>');
element.innerHTML = "<table border="1"><tr><td><strong>OMG</strong></td></tr></table>";
Separately I have 2 functions that do:
document.addEventListener("mouseover", function(e){
if (e.target.classList.contains("tooltip")) {
createTooltip(e);
}
});
document.addEventListener("mouseout", function(e){
if (e.target.classList.contains("tooltip")) {
removeAllTooltips();
}
});
I think your issue is that you're not actually appending the tooltip to anything. You need to append your element to a node that is already in the DOM. Here is a working example (without any CSS) that I got from your code, the only difference being that I appended the element node to an existing element in the DOM called root.
https://jsfiddle.net/irantwomiles/09o7vuj5/12/
I created a word counting function and found a discrepancy. It produced different results counting the text words in html depending on if the element the html is enclosed in is part of the document.body or not. For example:
html = "<div>Line1</div><div>Line2<br></div>";
document.body.insertAdjacentHTML("afterend", '<div id="node1"></div>');
node1 = document.getElementById("node1");
node1.style.whiteSpace = 'pre-wrap';
node1.innerHTML = html;
node2 = document.createElement('div');
node2.style.whiteSpace = 'pre-wrap';
node2.innerHTML = html;
The white-space: pre-wrap style is applied so that the code in the html variable is rendered, in terms of line-breaks, consistently across browsers. In the above:
node1.innerText // is "Line1\nLine2\n" which counts as two words.
node2.innerText // is "Line1Line2" which counts as only one word.
My word count function is:
function countWords(s) {
s = (s+' ').replace(/^\s+/g, ''); // remove leading whitespace only
s = s.replace(/\s/g, ' '); // change all whitespace to spaces
s = s.replace(/[ ]{2,}/gi,' ')+' '; // change 2 or more spaces to 1
return s.split(' ').filter(String).length;
}
If I then did something like this in the Web Console:
node1.after(node2);
node2.innerText // is changed to "Line1\nLine2\n" which counts as two words.
My questions are:
Why is the white-space: pre-wrap style not being applied to node 2.innerText before it is inserted into the document.body?
If node 2 has to be a part of document.body in order to get a white-space: pre-wrap style node 2.innerText value, how do I do that without having to make node 2 visible?
I'm curious. When I crate a node element with createElement, where does that node element reside? It doesn't appear to be viewable in a Web Console Inspector inside or outside of the <html> tag and I can't find it in the document object.
This tipped me off that the discrepancy was something to do with if the node element being in the document.body or not: javascript createElement(), style problem.
Indeed, when the element is attached to the DOM, Element.innerText takes the rendered value into account - you can say, the visible output. For non-attached elements, there is no rendering. The CSS properties exist but are not executed.
If you want consistent results between attached and non-attached elements, use Element.textContent.
For more information, see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
In follow-up to my question above, I needed to count the words in html text strings like this: <div>Line1</div><div>Line2<br></div> where the word count matched what it would be if that html was rendered in the displayed DOM
To summarize what others have said, when you create an element using createElement it isn’t inserted into the DOM yet and can’t be found when inspecting the DOM. Before the element is inserted into the DOM, the CSS properties exist but are not executed, so there is no rendering. When the element is inserted into the DOM, the CSS properties are executed, and the element is rendered according to the CSS.
Here's the html-string-to-rendered-html-text function I ended up using. This function strips the html tags but retains the "white space" so that the words can then be counted (with consistency across browsers, including IE 11).
var html = "<div>Line1</div><div>Line2<br></div>";
// Display the html string
var htmlts = document.getElementById("htmlts");
htmlts.innerText = html;
// Display a DOM render of the html string
var node1 = document.getElementById("node1");
node1.style.whiteSpace = 'pre-wrap';
node1.innerHTML = html;
// Display the innerText of the above DOM render
var node1ts = document.getElementById("node1ts");
node1ts.innerText = node1.innerText;
// Display the results of the htmlToText function
var node2ts = document.getElementById("node2ts");
node2ts.innerText = htmlToText(html);
// Adapted from https://stackoverflow.com/a/39157530
function htmlToText(html) {
var temp = document.createElement('div');
temp.style.whiteSpace = 'pre-wrap';
temp.style.position = "fixed"; // Overlays the normal flow
temp.style.left = "0"; // Placed flush left
temp.style.top = "0"; // Placed at the top
temp.style.zIndex = "-999"; // Placed under other elements
// opacity = "0" works for the entire temp element, even in IE 11.
temp.style.opacity = "0"; // Everything transparent
temp.innerHTML = html; // Render the html string
document.body.parentNode.appendChild(temp); // Places just before </html>
var out = temp.innerText;
// temp.remove(); // Throws an error in IE 11
// Solution from https://stackoverflow.com/a/27710003
temp.parentNode.removeChild(temp); // Removes the temp element
return out;
}
<html lang="en-US">
<body>
HTML String: <code id="htmlts"></code><br><br>
Visible Render of HTML String (for comparison): <div id="node1"></div><br>
Visible Render Text String: <code id="node1ts"></code><br>
Function Returned Text String: <Code id="node2ts"></code><br>
</body>
</html>
If you prefer to have the temporary element insert inside the body element, change document.body.parentNode.appendChild to document.body.appendChild.
As Noam had suggested, you can also use temp.style.top = "-1000px";.
To answer my curiosity question: before the element is "inserted into the DOM" it appears to be in a Shadow DOM or Shadow Dom-like space.
I am trying to write a ToDoList with JavaScript.
I have an input-element. Whenever I type something and press enter, it creates a new fieldset(in my example its a fieldset but it can also be a Div) with the class name ".fieldListClass" and a P-Tag as a child of fieldset. the P-tag innerHTML is the the value of input. I used Click-EventListener for that.
After each click, I assigned the query selector of all .fieldListClass to a nodeList "fieldListQuery". I even converted this nodeList into an Array but no result.
Now I want to create an addEventListner but outside the previous one. it should be a new one. And It should be a click-EventListener for all fieldListQuery which where created inside the previous function.(this part is at the bottom of my code)
When I click on it something should happen like removing the current target etc. But it wont work because outside the function it always says that this variable is undefined. I don't get it because I declared it global outside of the function.
I don't want to use DOMNodeInserted or MutationObserver yet for detecting changes inside the DOM. Simple because the first one is not recommended anymore it and the last one I have no idea how to use it. Many people saying that this is not a safe way.
Any Help please?
let addDiv = document.createElement("div"); addDiv.id = "addDivId";
let listDiv = document.createElement("div"); listDiv.id = "listDivId";
let inputText = document.createElement("input"); inputText.id = "inputTextId";
let fieldList; // = document.createElement("fieldset");
let fieldDiv; // = document.createElement("div");
let fieldDivP; // = document.createElement("P");
let fieldListArr;
let fieldListQuery;
document.body.appendChild(addDiv);
addDiv.appendChild(inputText);
document.body.appendChild(listDiv);
inputText.addEventListener("keypress", event => {
if (event.key === "Enter") {
fieldList = document.createElement("fieldset");
fieldDiv = document.createElement("div");
fieldDivP = document.createElement("P");
listDiv.appendChild(fieldList);
fieldList.className = "fieldListClass";
fieldList.appendChild(fieldDiv);
fieldDiv.appendChild(fieldDivP);
fieldDivP.innerHTML = inputText.value;
fieldListQuery = document.querySelectorAll(".fieldListClass") ;
}
})
fieldListQuery.forEach(element => { // <- it say fieldListQuery is undefined.
fieldListQuery.addEventListener("click", e => {
e.currentTarget.innerHTML="test";
})
});
´´´
Since I offered critique of your approach, I thought it is only fair I at least try to offer you some code that accomplishes (on the overall level, in light of absence of much detail about your solution) something along of what you have.
First off, I think creating trees of elements through a script when other solutions are more viable, tends to show an anti-pattern. Your script is invariably loaded in the context of an HTML document, which may already contain a lot of useful markup -- including an input field (that you were creating with createElement). If the input field is a "constant" there is no need to waste code on creating it -- just put it in your markup.
Second, even for elements or hierarchies of elements that are created "on demand" -- as a reaction to an event or however else -- it typically is much more readable and manageable to use templates. As a fallback -- if template cannot be used for some reason -- using innerHTML to create entire element trees is actually an appealing and more readable option than a lot of "boilerplate" containing createElement, appendChild, etc.
Third, you should always try to see if you can have your interactive controls be part of a form. I won't go into all reasons to do so, but suffice to say it helps user agents that screen-read content and for other accessibility systems, to name one. There are exceptions to this rule, but I don't recall looking at code where a control should not be part of a form -- so the rule is a good one.
Here is a proof-of-concept bare-bones to-do application:
<html>
<head>
<script>
function submit_create_todo_item_form() {
const new_todo_fragment = document.getElementById("todo-item-template").content.cloneNode(true);
new_todo_fragment.querySelector(".body").textContent = document.forms[0].elements[0].value;
document.body.appendChild(new_todo_fragment);
}
</script>
<template id="todo-item-template">
<div class="todo-item">
<p class="body"></p>
</div>
</template>
</head>
<body>
<form action="javascript: submit_create_todo_item_form()">
<input>
</form>
</body>
<html>
Take note that I use textContent instead of innerHTML to create content for a to-do item's body. innerHTML invokes the HTML parser and unless you plan to be typing hypertext into that single line of input field, innerHTML only costs you extra for no clear benefit. If you need to interpret the value verbatim, textContent is instead exactly what's needed. So, approach your solution with that in mind.
I hope this is useful, I worked with what I thought I had.
If I do this:
document.getElementById("myDiv").innerHTML = "some_html_code";
will that create nodes in my DOM three as it would if I used appendChild()?
Reason for asking is that I'm creating a mobile application where memory usage must be low. I don't want to create a lot of nodes.
It is roughly the same as
var div = document.getElementById("myDiv");
while( div.firstChild ) {
div.removeChild( div.firstChild );
}
div.appendChild( document.createTextNode("a_html_string") );
Of course, if by "html_string" you mean a string consisting of complex html, then of course nodes are created from the html as appropriate (element nodes, comment nodes, text nodes etc). But a simple string of text is simply a single text node.
So if your html string were '<div id="hello">world</div>', that would roughly be:
var div = document.getElementById("myDiv");
while( div.firstChild ) {
div.removeChild( div.firstChild );
}
var anotherDiv = document.createElement("div");
anotherDiv.setAttribute("id", "hello");
anotherDiv.appendChild(document.createTextNode("world"));
div.appendChild(anotherDiv);
It is probably shocking how much is happening with a simple innocent looking .innerHTML setter, and this is not even including parsing the html.
It's important to note that none of this is garbage, all of those objects created are necessary. To make sure you are only creating necessary nodes, do not use unnecessary whitespace between nodes. For example
<span>hello</span> <span>world</span>
is 3 text nodes but
<span>hello</span><span> world</span>
is only 2 text nodes.
A long while ago I created a facetious jsfiddle that converts html to "DOM code" for all of those .innerHTML haters.
Yes, the innerHTML will be parsed and the nodes will be created. There's no way around it, the DOM is made of nodes.
It will create string "a_html_string" and it will be displayed. but you can also append elements:
document.getElementById("myDiv").innerHTML = "<a> Link </a>"; // will be displayed "Link"
I've got a loop where I insert about 500 <div>s into the DOM via document.getElementById(target).appendChild(docFragmentDiv), and each <div> has probably 20 or 30 descendants. I can insert up to about 100 just fine, but the page temporarily freezes while the DOM is updated if there's something like 200+ inserts.
How can I avoid this choke? Right now I have an interval set up to insert 50 per interval, with 25 ms between intervals. Is there a better way to do this?
You should use innerHTML, and you should use it only once.
For optimal efficiency, you should assemble pieces of HTML in an array of strings using a loop, then write document.getElementById(target).innerHTML = htmlPieces.join()
Using innerHTML like SLaks suggests will work, but it's probably better practice to create a document fragment, append all of your div nodes to that, then append the document fragment to the main document.
var docFrag = document.createDocumentFragment();
for (var i=0; i<500; i++) {
docFrag.appendChild(document.createElement("div"));
}
document.getElementById(target).appendChild(docFrag);
innerHtml() is almost always faster than appendChild().
Have a look at QuirksMode's benchmarking for real numbers.
Edit: Is there any reason why you're adding them in intervals rather than all at once? I think SLaks is right about doing it all at once.
I'm not sure if this would be any faster, but it would be the first thing I tried:
Instead of adding them all to the page, add them all to a container that you haven't yet added to the page, and then add that container to the page.
If that improves performance, then you can modify it to suit your needs.
Be careful that you aren't using appendChild to the actual document until the last instant. In other words, if you're doing:
var div1 = document.createElement("div");
document.body.appendChild(div1);
for (var i=0; i<500; i++) {
var div = document.createElement("div");
div1.appendChild(div);
}
Then you are redrawing the entire page every time you iterate in that loop.
Better, if you're using DOM methods, to reverse this:
var div1 = document.createElement("div");
for (var i=0; i<500; i++) {
var div = document.createElement("div");
div1.appendChild(div);
}
document.body.appendChild(div1);
Now you are drawing your divs to an offscreen buffer, and only making the page redraw once.