Certain elements not allowed in DocumentFragment? - javascript

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.

Related

what's the difference between document.createElement and createDocumentFragment? [duplicate]

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.

Native Javascript doesn't append element on the DOM

I have a website built in Expression Engine. In the back-end there is a code snippet that takes care of a JavaScript request and build a page based on the request.
I have a HTML Page without head tag.
This page is without styling
Sample:
<div class="top-arrow"><p><!--- Rest of code --></p>
</div>
<!-- Html page continues-->
I have added the following code in my attempt and it doesnt seem to work.
var span = document.createElement("span"); //Test element
span.textContent = "A <span> element.";
var node = document.getElementsByClassName("top-arrow");
node.insertBefore(span);
Below is what I get:
TypeError: node.insertBefore is not a function
node.insertBefore(span);
How best can I append text before the div with plain JavaScript.
getElementsByClassName will return array-like node-list which does not have method insertBefore
The Node.insertBefore(newNode, referenceNode) method inserts the specified node before the reference node as a child of the current node(If referenceNode is null, the newNode is inserted at the end of the list of child nodes)
Note: referenceNode is not an optional argument, if there is no ant ref node, pass null
Try this:
var span = document.createElement("span");
span.textContent = "A <span> element.";
var node = document.getElementsByClassName("top-arrow")[0];
//_____________________________________________________^^(Get the first element from collection)
node.insertBefore(span, null);
<div class="top-arrow">
<p>
</p>
</div>
document.getElementsByClassName("top-arrow") will return a live HTMLCollection. You can use it like an array:
node = document.getElementsByClassName("top-arrow")[0];
Also, if you want the new node to appear before top-arrow you need to do:
node.parentNode.insertBefore(span, node);
As it is node has no children, so there is no need to do insertBefore.
Even though your HTML code has no body and head, the browser will 'fix' your HTML and add one.
I would write your code like this:
var span = document.createElement("span"); //Test element
span.appendChild(document.createTextNode("A <span> element."));
var node = document.getElementsByClassName("top-arrow")[0];
node.parentNode.insertBefore(span, node);
Function getElementsByClassName() returns an array containing nodes with class specified. If you want to insertBefore or append anything to it you need to specify index of an element in this array. Also, insertBefore requires two arguments in function call (elementToInsert, elemenBeforeWhichYouWantToInsert). So, something like this should work:
document.getElementsByClassName('top-arrow')[0].insertBefore(element, beforeWhatToInsert);
Thank you guys for all your input they are very informative. I have solved this without the need of manipulating my DOM element by simply copying the dynamic part of the page and actually creating a new template in the back-end of Expression Engine and my problem was solved.

jquery.html() returns only first text when loaded from string

Supposedly a jquery object can be initialized from string. This can often happen when processing ajax results, i.e. I'm trying to replicate http://api.jquery.com/jQuery.post/
However, I'm seeing strange behavior:
function test() {
var content = $("<html><body><div>hello</div><div>world</div></body></html>");
alert("content.text() = " + content.text());
alert("content.html() = " + content.html());
}
The first alert shows: content.text() = helloworld
The second alert shows: content.html() = hello
What's happening here?
Solution
Thanks everyone for the explanations. I ended up adding another layer of <div> to have a single child of <body>, as in
<html>
<body>
<div> <=== added
<div>hello</div>
<div>world</div>
</div>
</body>
</html>
When parsing HTML fragments containing a body element, browsers in general (and jQuery does this as well) will disregard everything except what's inside the body element. So what you have there ends up being equivalent to:
var content = $("<div>hello</div><div>world</div>");
alert("content.text() = " + content.text());
alert("content.html() = " + content.html());
You end up with a jQuery object with two elements in it: The div elements.
In jQuery, normally accessor functions (html, val, css, etc.) only use the first element in the set when you use them as getters, and that's what html is doing above. text is an unusual accessor function in jQuery: It gives you the combined text of all of the elements in the set, not just the first.
We can see this in the docs, but it's still surprising. From html:
Get the HTML contents of the first element in the set of matched elements or set the HTML contents of every matched element.
From text:
Get the combined text contents of each element in the set of matched elements, including their descendants, or set the text contents of the matched elements.
(My emphasis in both cases.)
Browsers remove those html and body elements. The content collection has only 2 div elements.
When passing in complex HTML, some browsers may not generate a DOM that exactly replicates the HTML source provided. As mentioned, jQuery uses the browser's .innerHTML property to parse the passed HTML and insert it into the current document. During this process, some browsers filter out certain elements such as <html>, <title>, or <head> elements. As a result, the elements inserted may not be representative of the original string passed.
This is the string representation of your content collection:
content.map(function() { return this.outerHTML || this.nodeValue; }).get().join('');
// -> <div>hello</div><div>world</div>
.text() method returns textContent/nodeValue of all the elements in the collection:
content.text(); // -> helloworld
.and .html() method returns innerHTML of the first element in the collection:
content.html(); // -> hello
This is what your content looks like to a browser:
"content = " Object { 0: <div>, 1: <div>, length: 2 }
Basically this is in some kind a set of 2 elements.
And here's what http://api.jquery.com/html/ say:
Get the HTML contents of the first element in the set of matched elements

How to write to a <div> element using JavaScript?

I've searched around using Google and Stack Overflow, but I haven't seemed to find a answer to this. I want to write text inside a <div> element, using JavaScript, and later clear the <div> element, and write more text into it. I am making a simple text adventure game.
This is what I am trying to do:
<DOCTYPE!HTML>
<body>
<div class="gamebox">
<!-- I want to write in this div element -->
</div>
</body>
As a new user to JavaScript, how would I be able to write inside the div element gamebox? Unfortunately, my JavaScript skills are not very good, and it would be nice if you can patiently explain what happens in the code.
You can use querySelector to get a reference to the first element matching any CSS selector. In your case, a class selector:
var div = document.querySelector(".gamebox");
querySelector works on all modern browsers, including IE8. It returns null if it didn't find any matching element. You can also get a list of all matching elements using querySelectorAll:
var list = document.querySelectorAll(".gamebox");
Then you access the elements in that list using 0-based indexes (list[0], list[1], etc.); the length of the list is available from list.length.
Then you can either assign HTML strings to innerHTML:
div.innerHTML = "This is the text, <strong>markup</strong> works too.";
...or you can use createElement or createTextNode and appendChild / insertBefore:
var child = document.createTextNode("I'm text for the div");
div.appendChild(span); // Put the text node in the div
Those functions are found in the DOM. A lot of them are now covered in the HTML5 specification as well (particularly Section 3).
Select a single element with document.querySelector or a collection with document.querySelectorAll.
And then it depends, on what you want to do:
Writing Text into the div or create an Element and append it to the div.
Like mentioned getElementsByClassName is faster. Important to know it when you use this you get returned an array with elements to reach the elment you want you specify its index line [0], [1]
var gameBox = document.getElementsByClassName('gamebox')[0];
Here how you can do it
//returns array with elements
var gameBox = document.getElementsByClassName('gamebox');
//inner HTML (overwrites fsd) this can be used if you direcly want to write in the div
gameBox[0].innerHTML ='<p>the new test</p>';
//Appending when you want to add extra content
//create new element <p>
var newP = document.createElement('p');
//create a new TextNode
var newText = document.createTextNode("i'm a new text");
//append textNode to the new element
newP.appendChild(newText);
//append to the DOM
gameBox[0].appendChild(newP);
https://developer.mozilla.org/en-US/docs/Web/API/document.createElement
https://developer.mozilla.org/en-US/docs/Web/API/document.getElementsByClassName

Surrounding individual words inside HTML text with SPAN tags?

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

Categories

Resources