72

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.

Domenic
  • 110,262
  • 41
  • 219
  • 271

11 Answers11

107

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/

dandavis
  • 16,370
  • 5
  • 40
  • 36
  • 6
    Not supported by IE at all, so "modern browsers" is a bit misleading. – Marcello di Simone Feb 24 '15 at 21:00
  • 1
    Almost upvoted, if it weren't that `` tag in the demo. – Jonatas Walker Aug 11 '15 at 20:13
  • 4
    An issue with createContextualFragment is that, html like 'test' 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. – Munawwar Oct 24 '15 at 18:47
  • Do you have any idea for nice Safari alternative? or at least an issue in their tracker? – tomalec Dec 22 '15 at 19:19
  • 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 Nov 05 '18 at 14:26
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);
Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • 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. – Domenic Feb 15 '12 at 17:13
  • Misses text between elements.... `var elemQueried = document.createDocumentFragment(); var tmp = document.createElement('body'), child; tmp.innerHTML = '
    x
    Blehy'; var children = tmp.childNodes; while(children.length){ elemQueried.appendChild(children[0]); }`
    – PAEz Jun 16 '15 at 16:27
  • @PAEz Posted an answer below that takes care of this issue. – Matt May 16 '16 at 17:57
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.

Pete Blois
  • 214
  • 3
  • 4
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);
Patrick Dark
  • 2,187
  • 1
  • 20
  • 23
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.

Matt
  • 3,617
  • 2
  • 27
  • 39
  • 5
    Even conciser: `.firstChild` - `while (intermediateContainer.firstChild) fragement.appendChild(intermediateContainer.firstChild);` – Rob W May 16 '16 at 18:00
  • Good suggestion. I suppose it's a question of style at this point. Thanks! – Matt May 16 '16 at 18:51
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

Michal
  • 13,439
  • 3
  • 35
  • 33
  • 1
    Yeah, I don't want the extra container element. – Domenic Feb 15 '12 at 17:10
  • 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 – Michal Feb 15 '12 at 18:51
  • 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. – Domenic Feb 15 '12 at 19:11
  • 1
    @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. – Christophe Sep 08 '13 at 19:12
  • @Domenic frag = holder.cloneNode(true); – B.F. Mar 02 '14 at 05:16
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;
}
Tobias Buschor
  • 3,075
  • 1
  • 22
  • 22
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.

Ballpin
  • 217
  • 2
  • 6
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;
        }
    }
bnieland
  • 6,047
  • 4
  • 40
  • 66
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);
Guest
  • 1
  • 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 `appendChild` – KevBot Feb 28 '17 at 02:13
-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]);
Morgan T.
  • 1,937
  • 1
  • 17
  • 20
  • @RobW. That's not a good reason to down vote if the OP wrote _" jQuery is allowed"_ – gdoron Feb 14 '12 at 22:30
  • @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. – Rob W Feb 14 '12 at 22:37
  • @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 – Morgan T. Feb 15 '12 at 12:32
  • Yeah, same problem as Michal's answer; this does not satisfy the problem statement, since it inserts an extra wrapper `div#main`. – Domenic Feb 15 '12 at 17:11