Inserting arbitrary HTML into a DocumentFragment
Asked Answered
B

11

74

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.

Bellyache answered 14/2, 2012 at 21:0 Comment(0)
B
111

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/

Blouse answered 9/8, 2014 at 0:36 Comment(5)
Not supported by IE at all, so "modern browsers" is a bit misleading.Gaona
Almost upvoted, if it weren't that <font> tag in the demo.Merkel
An issue with createContextualFragment is that, html like '<td>test</td>' would ignore the td (and only create 'test' text node). template tag solution is the way to go. BTW Edge 13 supports template tag now.Marseillaise
Do you have any idea for nice Safari alternative? or at least an issue in their tracker?Gambado
I've needed array off element nodes from Fragement and this works in Chrome and IE11 [].slice.call(temp.content ? temp.content.children : temp.childNodes)Disgraceful
H
24

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);
Harry answered 14/2, 2012 at 22:17 Comment(3)
Bleh, I guess this is the only way to go. At least you actually answered the question and satisfied the problem statement :). Still, it's annoying to have to loop; ah well.Bellyache
Misses text between elements.... var elemQueried = document.createDocumentFragment(); var tmp = document.createElement('body'), child; tmp.innerHTML = '<div>x</div>Bleh<span>y</span>'; var children = tmp.childNodes; while(children.length){ elemQueried.appendChild(children[0]); }Marigolde
@Marigolde Posted an answer below that takes care of this issue.Dogs
N
12

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.

Nan answered 10/8, 2014 at 5:56 Comment(3)
This should be the selected answer and deserves way more upvotes <3Millrace
Careful! not standardized.Antiphonary
It does not work with td, th elements for table.Ideal
M
10

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);
Milepost answered 27/6, 2019 at 19:38 Comment(2)
No doubt! This is the best answer! Both documentFragments and DOMParser are supported by older browsers, including IE9. Thumbs upMorganne
This misses nodes at the start that are valid in a <head> as well. Those will be put in the .head, not the .body.Tympanist
D
4

@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.

Dogs answered 16/5, 2016 at 17:56 Comment(2)
Even conciser: .firstChild - while (intermediateContainer.firstChild) fragement.appendChild(intermediateContainer.firstChild);Harry
Good suggestion. I suppose it's a question of style at this point. Thanks!Dogs
D
3

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

Dovecote answered 14/2, 2012 at 21:59 Comment(5)
Yeah, I don't want the extra container element.Bellyache
When you appending you always need an "extra" (or should I say a target) container - be a body or some other tag. If you don't want that it is loops for youDovecote
I guess I was hoping there would be a way to copy all children of holder into frag in one go (without looping). appendChildren or something. But it sounds like no such method exists.Bellyache
@Bellyache such a method exists...that's the whole point of document fragments ;-) your real issue here is that you're dealing with a html string, not html elements.Nonstandard
@Bellyache frag = holder.cloneNode(true);Epistyle
H
2

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;
}
Heeling answered 8/4, 2017 at 11:16 Comment(1)
Plus for supporting td in IE.Disgraceful
M
2

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.

Meagan answered 19/10, 2020 at 10:11 Comment(0)
E
1

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;
        }
    }
Exfoliate answered 17/8, 2017 at 14:46 Comment(0)
F
0
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);
Frants answered 17/2, 2017 at 16:5 Comment(1)
This does not work (at least in Chrome). When using insertAdjacentHTML inside of a DocumentFragment, you will get this error: Uncaught DOMException: Failed to execute 'insertAdjacentHTML' on 'Element': The element has no parent. Also, addChild is not a method, you should be using appendChildBrucine
S
-1

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]);
Skull answered 14/2, 2012 at 21:59 Comment(4)
@RobW. That's not a good reason to down vote if the OP wrote " jQuery is allowed"Lonesome
@Lonesome The main reason that I downvoted the answer is that the textual explanation is 100% wrong. A DocumentFragment object can have an arbitrary number of child elements. My previous comment was referring to Morgan's comment at Michal's answer.Harry
@RobW it was just a playful comment because we posted at the exact same time. I can see how others would see it as inappropriate though. I've removed it. I see what you mean about my logic too. During my testing I made a mistake and based my explanation off that. I've updated my answer. ThanksSkull
Yeah, same problem as Michal's answer; this does not satisfy the problem statement, since it inserts an extra wrapper div#main.Bellyache

© 2022 - 2024 — McMap. All rights reserved.