Inserting arbitrary HTML into a DocumentFragment

35,888

Solution 1

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/

Solution 2

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);

Solution 3

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.

Solution 4

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);

Solution 5

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

Share:
35,888
Domenic
Author by

Domenic

I'm a developer on the Google Chrome team, working to make the web platform more awesome. I work on web specs at the WHATWG, including editing the HTML Standard and Streams Standard. I help improve the JavaScript standard at Ecma TC39, representing Chrome's interest there and proposing some new features for future inclusion. Once upon a time I was elected to the W3C TAG to ponder issues of web architecture. In my spare time, my hobbyist programming centers around lots of Node.js and web apps. My favorite project is currently jsdom. I also used to present a lot at conferences. Check me out on GitHub and npm.

Updated on October 20, 2020

Comments

  • Domenic
    Domenic over 3 years

    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.

  • gdoron is supporting Monica
    gdoron is supporting Monica over 12 years
    @RobW. That's not a good reason to down vote if the OP wrote " jQuery is allowed"
  • Rob W
    Rob W over 12 years
    @gdoron 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.
  • Morgan T.
    Morgan T. over 12 years
    @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. Thanks
  • Domenic
    Domenic over 12 years
    Yeah, I don't want the extra container element.
  • Domenic
    Domenic over 12 years
    Yeah, same problem as Michal's answer; this does not satisfy the problem statement, since it inserts an extra wrapper div#main.
  • Domenic
    Domenic over 12 years
    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.
  • Michal
    Michal over 12 years
    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 you
  • Domenic
    Domenic over 12 years
    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.
  • Christophe
    Christophe almost 11 years
    @Domenic 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.
  • B.F.
    B.F. over 10 years
    @Domenic frag = holder.cloneNode(true);
  • Marcello di Simone
    Marcello di Simone over 9 years
    Not supported by IE at all, so "modern browsers" is a bit misleading.
  • PAEz
    PAEz about 9 years
    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]); }
  • Jonatas Walker
    Jonatas Walker almost 9 years
    Almost upvoted, if it weren't that <font> tag in the demo.
  • Munawwar
    Munawwar over 8 years
    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.
  • tomalec
    tomalec over 8 years
    Do you have any idea for nice Safari alternative? or at least an issue in their tracker?
  • Matt
    Matt about 8 years
    @PAEz Posted an answer below that takes care of this issue.
  • Rob W
    Rob W about 8 years
    Even conciser: .firstChild - while (intermediateContainer.firstChild) fragement.appendChild(intermediateContainer.firstChild);
  • Matt
    Matt about 8 years
    Good suggestion. I suppose it's a question of style at this point. Thanks!
  • KevBot
    KevBot over 7 years
    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 appendChild
  • jcubic
    jcubic over 5 years
    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)
  • jcubic
    jcubic over 5 years
    Plus for supporting td in IE.
  • Johan
    Johan about 5 years
    This should be the selected answer and deserves way more upvotes <3
  • Victoria
    Victoria over 4 years
    Careful! not standardized.
  • Bharata
    Bharata almost 4 years
    It does not work with td, th elements for table.
  • Uchenna Ajah
    Uchenna Ajah over 2 years
    No doubt! This is the best answer! Both documentFragments and DOMParser are supported by older browsers, including IE9. Thumbs up