I know that adding innerHTML to document fragments has been recently discussed, and will hopefully see inclusion in the DOM Standard. But, what is the workaround you're supposed to use in the meantime?
That is, take
var html = '<div>x</div><span>y</span>';
var frag = document.createDocumentFragment();
I want both the div and the span inside of frag, with an easy one-liner.
Bonus points for no loops. jQuery is allowed, but I've already tried $(html).appendTo(frag); frag is still empty afterward.
Here is a way in modern browsers without looping:
var temp = document.createElement('template');
temp.innerHTML = '<div>x</div><span>y</span>';
var frag = temp.content;
or, as a re-usable
function fragmentFromString(strHTML) {
var temp = document.createElement('template');
temp.innerHTML = strHTML;
return temp.content;
}
UPDATE:
I found a simpler way to use Pete's main idea, which adds IE11 to the mix:
function fragmentFromString(strHTML) {
return document.createRange().createContextualFragment(strHTML);
}
The coverage is better than the <template> method and tested ok in IE11, Ch, FF.
Live test/demo available http://pagedemos.com/str2fragment/
Currently, the only way to fill a document fragment using only a string is to create a temporary object, and loop through the children to append them to the fragment.
Since it's not appended to the document, nothing is rendered, so there's no performance hit.
You see a loop, but it's only looping through the first childs. Most documents have only a few semi-root elements, so that's not a big deal either.
If you want to create a whole document, use the DOMParser instead. Have a look at this answer.
Code:
var frag = document.createDocumentFragment(),
tmp = document.createElement('body'), child;
tmp.innerHTML = '<div>x</div><span>y</span>';
while (child = tmp.firstElementChild) {
frag.appendChild(child);
}
A one-liner (two lines for readability) (input: String html, output: DocumentFragment frag):
var frag =document.createDocumentFragment(), t=document.createElement('body'), c;
t.innerHTML = html; while(c=t.firstElementChild) frag.appendChild(c);
Use Range.createContextualFragment:
var html = '<div>x</div><span>y</span>';
var range = document.createRange();
// or whatever context the fragment is to be evaluated in.
var parseContext = document.body;
range.selectNodeContents(parseContext);
var fragment = range.createContextualFragment(html);
Note that the primary differences between this approach and the <template> approach are:
Range.createContextualFragment is a bit more widely supported (IE11 just got it, Safari, Chrome and FF have had it for a while).
Custom elements within the HTML will be upgraded immediately with the range, but only when cloned into the real doc with template. The template approach is a bit more 'inert', which may be desirable.
No one ever provided the requested "easy one-liner".
Given the variables…
var html = '<div>x</div><span>y</span>';
var frag = document.createDocumentFragment();
… the following line will do the trick (in Firefox 67.0.4):
frag.append(...new DOMParser().parseFromString(html, "text/html").body.childNodes);
#PAEz pointed out that #RobW's approach does not include text between elements. That's because children only grabs Elements, and not Nodes. A more robust approach might be as follows:
var fragment = document.createDocumentFragment(),
intermediateContainer = document.createElement('div');
intermediateContainer.innerHTML = "Wubba<div>Lubba</div>Dub<span>Dub</span>";
while (intermediateContainer.childNodes.length > 0) {
fragment.appendChild(intermediateContainer.childNodes[0]);
}
Performance may suffer on larger chunks of HTML, however, it is compatible with many older browsers, and concise.
createDocumentFragment creates an empty DOM "container". innerHtml and other methods work only on DOM nodes (not the container) so you have to create your nodes first and then add them to the fragment. You can do it using a painful method of appendChild or you can create one node and modify it's innerHtml and add it to your fragment.
var frag = document.createDocumentFragment();
var html = '<div>x</div><span>y</span>';
var holder = document.createElement("div")
holder.innerHTML = html
frag.appendChild(holder)
with jquery you simply keep and build your html as a string. If you want to convert it to a jquery object to perform jquery like operations on it simply do $(html) which creates a jquery object in memory. Once you are ready to append it you simply append it to an existing element on a page
Like #dandavis said, there is a standard way by using the template-tag.
But if you like to support IE11 and you need to parse table elements like '<td>test', you can use this function:
function createFragment(html){
var tmpl = document.createElement('template');
tmpl.innerHTML = html;
if (tmpl.content == void 0){ // ie11
var fragment = document.createDocumentFragment();
var isTableEl = /^[^\S]*?<(t(?:head|body|foot|r|d|h))/i.test(html);
tmpl.innerHTML = isTableEl ? '<table>'+html : html;
var els = isTableEl ? tmpl.querySelector(RegExp.$1).parentNode.childNodes : tmpl.childNodes;
while(els[0]) fragment.appendChild(els[0]);
return fragment;
}
return tmpl.content;
}
Here is a x-browser solution, tested on IE10, IE11, Edge, Chrome and FF.
function HTML2DocumentFragment(markup: string) {
if (markup.toLowerCase().trim().indexOf('<!doctype') === 0) {
let doc = document.implementation.createHTMLDocument("");
doc.documentElement.innerHTML = markup;
return doc;
} else if ('content' in document.createElement('template')) {
// Template tag exists!
let el = document.createElement('template');
el.innerHTML = markup;
return el.content;
} else {
// Template tag doesn't exist!
var docfrag = document.createDocumentFragment();
let el = document.createElement('body');
el.innerHTML = markup;
for (let i = 0; 0 < el.childNodes.length;) {
docfrag.appendChild(el.childNodes[i]);
}
return docfrag;
}
}
I would go with something like this..
function fragmentFromString(html) {
const range = new Range();
const template = range.createContextualFragment(html);
range.selectNode(template.firstElementChild);
return range;
}
// Append to body
// document.body.append(fragmentFromString(`<div>a</div>`).cloneContents())
This way you keep the content inside a Range object and you get all the needed methods for free.
You can find the list of all Range methods and properties here https://developer.mozilla.org/en-US/docs/Web/API/Range
Note: Remember to use detatch() method once you are done with it to avoid leaks and improve performance.
var html = '<div>x</div><span>y</span>';
var frag = document.createDocumentFragment();
var e = document.createElement('i');
frag.appendChild(e);
e.insertAdjacentHTML('afterend', html);
frag.removeChild(e);
To do this with as little lines as possible, you could wrap your content above in another div so you do not have to loop or call appendchild more than once. Using jQuery (as you mentioned is allowed) you can very quickly create an unattached dom node and place it in the fragment.
var html = '<div id="main"><div>x</div><span>y</span></div>';
var frag = document.createDocumentFragment();
frag.appendChild($(html)[0]);
Related
I have a relatively large (500-100 rows) HTML table with a bunch of <a> elements. I would like to add a <select> to the top of the page, and populate it by creating an <option> for each <a> in the table.
My first approach looked something like this:
var initSelect = function () {
var select = document.getElementById('mySelect');
var items = document.evaluate('//a', document, null, XPathResult.ANY_TYPE, null);
var item = items.iterateNext();
while (item) {
var elem = document.createElement("option");
var val = document.createAttribute("value");
val.value = elem.nodeValue;
elem.setAttributeNode(val);
elem.innerHTML = item.innerHTML;
select.appendChild(elem);
item = items.iterateNext();
}
};
window.onload = initSelect;
As soon as I tried to appendChild() to the <select> I got an UncaughtInvalidStateError. I figured that modifying the DOM was invalidating my XPathResult iterator, so I tried to add all of the <option> elements to an array first, and then appending them after iterating through all of the results.
var initSelect = function () {
var select = document.getElementById('src_select');
var items = document.evaluate('//a', document, null, XPathResult.ANY_TYPE, null);
var elems = [];
var item = items.iterateNext();
while (item) {
var elem = document.createElement("option");
var val = document.createAttribute("value");
val.value = elem.nodeValue;
elem.setAttributeNode(val);
elem.innerHTML = item.innerHTML;
elems.push(elem);
item = items.iterateNext();
}
for (var i = 0; i < elems.length; i++) {
select.appendChild(elems[i]);
}
};
window.onload = initSelect;
If I step through the code in the debugger, I see items.invalidIteratorState go to true after executing the elem.innerHTML = item.innerHTML line. Then I get the same error on the next call to items.iterateNext().
The first thing I'd like to get working is to just see the <select> populated. After that, the goal is to be able to select an element in the drop down, and have the page navigate to the same link that the corresponding <a> element would have taken me to.
This is the first JavaScript I've written, so I appreciate any and all feedback. At this point, I'm looking for a pure JavaScript solution. Once I get it working I'm going to try to pull JQuery in and revise it.
document.links gives you all a href elements in the document so there is no need to use the XPath API to access those elements. And if you are looking for elements in a particular parent then use e.g. document.getElementById('foo').getElementsByTagName('a') to find all a elements in that element with id attribute being foo. I don't see why you would need the DOM Level 3 XPath API for that, which is not supported in IE anyways. And neither document.links nor the result of getElementsByTagName can be invalidated like an XPath iterator result.
If you really want to use the XPath API then try a snapshot as the result type, it should not fail the way the iterator fails due to document manipulation.
I'm currently working on several Apps that include AJAX calls and I'm using JSON format to retrieve data from the server.
Each page needs to create content based on the JSON response, and I'm currently creating the content like:
function createBox1(json) {
var bigbox = document.createElement('div');
bigbox.className = 'class1';
var firstbox = document.createElement('div');
firstbox.className = 'first-box';
var firstNestedBox = document.createElement('div');
var secondNestedBox = document.createElement('div');
var thirdNestedBox = document.createElement('div');
var secondbox = document.createElement('div');
...
So basically its kinda a long code and I wanted to know if there is a better way to do it.
PS: I have seen some libraries where they do something like:
function o(t,e){var i=document.createElement(t||"div"),o;for(o in e)i[o]=e[o];return i}
and I suppose that's how they create multiple div elements, but I'm not sure how does that works.
Thanks in Advance (:
There's nothing wrong with the way you're doing it, but it's faster to just clone an empty div instead of creating new ones, and it's faster to use a fragment when building markup, and then insert the fragment in the DOM once everything is built instead of inserting each element in the DOM etc.
Just for making shorter code, a function could be used, but it's really just an uneccessary function call:
function createBox1(json) {
var div = document.createElement('div'),
bigbox = div.cloneNode(false),
firstbox = div.cloneNode(false),
firstNestedBox = div.cloneNode(false),
secondNestedBox = div.cloneNode(false),
thirdNestedBox = div.cloneNode(false),
secondbox = div.cloneNode(false):
bigbox.className = 'class1';
firstbox.className = 'first-box';
...
Is there a way to create a DOM object from the whole string, not just the innerHTML? I have a string in the form of a complete rendered DOM:
<some_tag_name class=... id=...>inner text</some_tag_name> (1)
and want to directly create a DOM object out of it. I know that there is a way to do:
e = document.createElement("some_tag_name")
e.innerHTML = ...
e.className = ...
e.id = ...
but when I do that, I have to extract the innerhtml part from the string (1) that I have, and analyze the tag type and all the attributes and assign that to e separately. I want to do all that simply from the string in the form of (1) that I have.
Edit
I followed the answers, but it was trickier than it seemed at first. The problem is that when you have a string representing things like tr, td, etc., and you try to put that as the innerHTML to a temporarily created div, the browser automatically adds extra tags outside of it. The following is my workaround to overcome this problem, where c is the string and e is the created element:
var cTagName = c.match(new RegExp('[a-zA-Z]+'))[0].toUpperCase();
var e = document.createElement("div");
e.innerHTML = c;
e = e.children[0];
//// When the type of `e' does not match what `c' expects, the browser
//// automatically modifies the type of c. The following is to undo this.
if(e.tagName.toUpperCase() != cTagName){
e = document.createElement("table");
e.innerHTML = c;
e = e.children[0];
};
if(e.tagName.toUpperCase() != cTagName){
e = document.createElement("tbody");
e.innerHTML = c;
e = e.children[0];
};
if(e.tagName.toUpperCase() != cTagName){
e = document.createElement("tr");
e.innerHTML = c;
e = e.children[0];
};
You can always do:
var div = document.createElement("div");
div.innerHTML = "<some> ... </some>"
var e = div.children[0];
(or if you're using jQuery, simply $("<some ... >")[0]).
You're looking for the outerHTML property.
var el = document.createElement('tag');
document.body.appendChild(el); // The element must be appended to the DOM before
// setting outerHTML. Otherwise, it will throw a
// NO_MODIFICATION_ALLOWED_ERR.
el.outerHTML='<some_tag_name class=... id=...>inner text</some_tag_name>';
Given that Firefox is a little behind the times on this one, it's probably safer to just create a wrapper div and set its innerHTML.
var el = document.createElement('div');
el.innerHTML = '<some_tag_name class=... id=...>inner text</some_tag_name>';
You can do it with jQuery:
var myDiv = $('<div class="my-div">This is my div!</div>');
I am currently have a chunk of string which actually a html source code stored in it. What I am trying to do now is to read out specific tags which I require using javascript. Can anyone help me with this, I am new to programming and I am not too sure how to go about it.
The problematic code:
if (request.readyState == 4) {
var html_text = request.responseText;
var parent = document.createElement('div');
parent.innerHTML = html_code;
var metas = parent.getElementsByTagName('meta');
var meta;
for (var i = 0; i < metas.length; i++) {
meta = metas[i];
alert(meta.property);
alert(meta.content);
}
}
The meta content works, but just that the meta property returned are undefined.
Use the DOM (Document Object Model) API. The Mozilla Dev Network (née Mozilla Dev Center) is a great starting point an all-around reference.
JavaScript Guide
The DOM and JavaScript
Traversing an HTML table with JavaScript and DOM Interfaces
What I am trying to do now is to read out specific tags which I require using javascript.
var text = /* whatever string that contains HTML */;
First you need to parse the string:
var parent = document.createElement('div');
parent.innerHTML = text;
Then you can search for whatever kind of element you're looking for. Say you're looking for <table> elements.
var tables = parent.getElementsByTagName('table');
Now you can do whatever you need to each element found:
var table;
for (var i=0, len=tables.length; i<len; i++)
{
table = tables[i];
// do something with the element
}
Relevant API docs
document.createElement
element.innerHTML
element.getElementsByTagName
Attributes of XML nodes are not readily available as DOM object properties. Use getAttribute
Sample: http://jsfiddle.net/mendesjuan/6Pdmw/
var node = document.createElement('div');
node.innerHTML = "<meta property='prop1' content='cont1'>"+
"<meta property='prop2' content='cont2'>";
var metas = node.getElementsByTagName('meta');
for (var i = 0; i < metas.length; i++) {
var meta = metas[i];
alert(meta.getAttribute("property"));
alert(meta.getAttribute("content"));
}
My goal is to remove all <[script]> nodes from a document fragment (leaving the rest of the fragment intact) before inserting the fragment into the dom.
My fragment is created by and looks something like this:
range = document.createRange();
range.selectNode(document.getElementsByTagName("body").item(0));
documentFragment = range.cloneContents();
sasDom.insertBefore(documentFragment, credit);
document.body.appendChild(documentFragment);
I got good range walker suggestions in a separate post, but realized I asked the wrong question. I got an answer about ranges, but what I meant to ask about was a document fragment (or perhaps there's a way to set a range of the fragment? hrmmm). The walker provided was:
function actOnElementsInRange(range, func) {
function isContainedInRange(el, range) {
var elRange = range.cloneRange();
elRange.selectNode(el);
return range.compareBoundaryPoints(Range.START_TO_START, elRange) <= 0
&& range.compareBoundaryPoints(Range.END_TO_END, elRange) >= 0;
}
var rangeStartElement = range.startContainer;
if (rangeStartElement.nodeType == 3) {
rangeStartElement = rangeStartElement.parentNode;
}
var rangeEndElement = range.endContainer;
if (rangeEndElement.nodeType == 3) {
rangeEndElement = rangeEndElement.parentNode;
}
var isInRange = function(el) {
return (el === rangeStartElement || el === rangeEndElement ||
isContainedInRange(el, range))
? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
};
var container = range.commonAncestorContainer;
if (container.nodeType != 1) {
container = container.parentNode;
}
var walker = document.createTreeWalker(document,
NodeFilter.SHOW_ELEMENT, isInRange, false);
while (walker.nextNode()) {
func(walker.currentNode);
}
}
actOnElementsInRange(range, function(el) {
el.removeAttribute("id");
});
That walker code is lifted from: Remove All id Attributes from nodes in a Range of Fragment
PLEASE No libraries (ie jQuery). I want to do this the raw way. Thanks in advance for your help
The easiest way to gather all <script> nodes would be to use getElementsByTagName, but unfortunately that is not implemented on DocumentFragment.
However, you could create a temporary container and append all elements within the fragment, and then go through and remove all <script> elements, like so:
var temp = document.createElement('div');
while (documentFragment.firstChild)
temp.appendChild(documentFragment.firstChild);
var scripts = temp.getElementsByTagName('script');
var length = scripts.length;
while (length--)
scripts[length].parentNode.removeChild(scripts[length]);
// Add elements back to fragment:
while (temp.firstChild)
documentFragment.appendChild(temp.firstChild);
Correct me if I'm wrong, but if the documentFragment is a real DOM Fragment, you should be able to do something like:
var scripts = documentFragment.getElementsByTagName('script');
if (scripts.length){
for (var i=0, l = scripts.length;i<l;i++){
documentFragment.removeChild(scripts[i]);
}
}
right?
Correction: you can't apply getElementsByTagName to a documentFragment, J-P is right. You can however us a child of the fragment if it is a (cloned) node supporting getElementsByTagName. Here's some (working) code I use within a larger script a few days ago:
var fragment = d.createDocumentFragment(), f;
fragment.appendChild(document.createElement('div'));
fragment.firstChild.appendChild(zoeklijst.cloneNode(true));
f = fragment.firstChild;
return f.getElementsByTagName(getList); //<==
2022, Chrome, querySelector family works on document fragments:
frag.content.querySelectorAll('script').forEach(
(s)=>s.remove()
);