Convert String to DOM Nodes

By  on  

My original post featured DOMParser, a JavaScript API for converting HTML strings into DOM nodes. While DOMParser works well in most cases, that API does have some rough edges and isn't as performant as another API: ContextualFragment. I've rewritten this post to highlight ContextualFragment, but if you still care to learn about DOMParser, please see the original text at the bottom of this post.

It wasn't too long ago that browsers were mostly stagnant when it came to implementing new APIs and features, leading to the rise of MooTools (FTW), jQuery, Dojo Toolkit, Prototype, and likewise JavaScript toolkits. Then we started doing more client side rendering and were forced to use a variety of tricks to handle templates, including massive HTML strings in our JavaScript and even abusing <script> tags to hold our templates.

Of course after you've placed your content into the template, you then need to turn that string into DOM nodes, and that process had a few of its own tricks, like creating an offscreen, dummy <div>, setting its innerHTML to the string value, grabbing the firstChild, and moving the node to its desired node. Each JavaScript toolkit would use its own strategy for converting string to DOM, highlighting the need for a standard method to accomplish this task.

Today there's a little known (but standard) way for converting string to DOM with JavaScript: ContextualFragment.

I've touched on DocumentFragment to create and store DOM nodes for performance in the past, but that post illustrated element creation via document.createElement:

// Use a DocumentFragment to store and then mass inject a list of DOM nodes
var frag = document.createDocumentFragment();
for(var x = 0; x < 10; x++) {
	var li = document.createElement("li");
	li.innerHTML = "List item " + x;
	frag.appendChild(li);
}

To create DOM nodes from a string of HTML we'll use document.createRange().createContextualFragment:

let frag = document.createRange().createContextualFragment('
One
Two
'); console.log(frag); /* #document-fragment
One
Two
*/

DocumentFragment objects share most of the methods that NodeList objects have, so you can use typical DOM methods like querySelector and querySelectorAll as well DOM traversal properties like firstChild with the resulting DocumentFragment:

let firstChild = frag.firstChild;

let firstDiv = frag.querySelector('div');
let allDivs = frag.querySelectorAll('div');

When you're ready to inject all of the created DOM nodes, you can simply execute:

// "placementNode" will be the parent of the nodes within the DocumentFragment
placementNode.appendChild(frag);

You can also inject nodes one at a time:

placementNode.appendChild(frag.firstChild);

The document.createRange().createContextualFragment function is an awesome, sane method for converting strings to DOM nodes within JavaScript. Ditch your old shims and switch to this performant, simple API!

The Original Post: DOMParser

Today we have a standard way for converting string to DOM with JavaScript: DOMParser.

The JavaScript

All you need to do is create a DOMParser instance and use its parseFromString method:

let doc = new DOMParser().parseFromString('<div><b>Hello!</b></div>', 'text/html');

Returned is a document containing the nodes generated from your string. With said document you can use standard node traversal methods to retrieve the nodes we specified in our string:

let doc = new DOMParser().parseFromString('<div><b>Hello!</b></div>', 'text/html');
let div = doc.body.firstChild;

let divs = doc.body.querySelectorAll('div');

You don't need a single wrapping element like JSX components -- you can have sibling elements:

let doc = new DOMParser().parseFromString('<div>1</div><div>2</div>', 'text/html');
let firstDiv = doc.body.firstChild;
let secondDiv = firstDiv.nextSibling;

Here's a simple wrapping function for DOMParser to retrieve the nodes:

let getNodes = str => new DOMParser().parseFromString(str, 'text/html').body.childNodes;

let nodes = getNodes('<div>1</div><div>2</div>');

// [div, div] 

The DOMParser object is an awesome, sane method for converting strings to DOM nodes within JavaScript. Ditch your old shims and switch to this efficient, simple API!

Recent Features

  • By
    Page Visibility API

    One event that's always been lacking within the document is a signal for when the user is looking at a given tab, or another tab. When does the user switch off our site to look at something else? When do they come back?

  • By
    I&#8217;m an Impostor

    This is the hardest thing I've ever had to write, much less admit to myself.  I've written resignation letters from jobs I've loved, I've ended relationships, I've failed at a host of tasks, and let myself down in my life.  All of those feelings were very...

Incredible Demos

Discussion

  1. parser

    Can’t you do this like 100 times faster and easier with jQuery using

    $('Hello!')
    • _xxx

      with jQuery <- exactly the problem with your argument. not to mention pretty big difference in performance.

    • Thats the whole point of this article — present a native, free of libraries way of doing it.

  2. Hristo Chakarov

    Some old technique:

    var tmp = document.createElement( 'div' );
    tmp.innerHTML = 'Hello!';
    var elements = tmp.childNodes;
    
    • Bobby

      Thank you for sharing this technique! Definitely learn something new.

  3. Danny Engelman
    • David Baker

      I tried running those performance tests and DOMParser came out the slowest for both. Am I missing something?

      Link to test results: http://imgur.com/a/scy0Q

  4. Owen

    How do I get the HTML back out (for example, what if the html that went in has a style-sheet)? Using doc.body.innerHTML doesn’t have that or any header or other information in it… I want all the marked-up text of the whole document…

  5. Owen

    I was able to get all of the html by wrapping the string with body tags. Sort of a kludge, but it works for what I need…

  6. Stephen Thomas

    createContextualFragment() only works if the fragment is a valid child of the element.

    The following, for example, *doesn’t* work:

    document.createRange().createContextualFragment('table cell');
    
  7. Andfinally

    I’m getting weird results when trying to append a fragment to a table – the fragment is appended as text.

    https://codepen.io/andfinally/pen/vWqwxB

  8. Andfinally

    Whoops, I’ve just discovered why my table row example didn’t work. The HTML string has to be valid if inserted into a document – so if you have a you must have the tag. The parsing process acts as though you’re creating a new HTML document, which of course you are.

  9. Mr. Walsh, your post safeguarded my sanity this evening and kept me from dashing my poor MacBook against the pavement. Thank you! =)

  10. Was sad to see Scriptaculous missing from your list of early libraries.

  11. Kris

    Today I discovered that in MS Edge, if you try to use querySelector on the fragment, to addEventListeners or things of the like, you get an error message saying that “object does not support querySelector”. Spent a good chunk of the day trying to work around it until I came across your older method, which seems to work just fine.

    I expect this won’t be an issue once “Chredge” is released.

    https://gist.github.com/krivaten/90558c7786a97826a741dfb8533f532c

    • Symon

      You’re actually getting a Text node when you use firstChild, this depends on the string you pass on to your createFragment function. Use firstElementChild instead.

      function createFragment(markup) {
        const fragment = document.createRange().createContextualFragment(markup);
        return fragment.firstElementChild;
      }
      
    • Kris

      Thank you for the response!

  12. J. Redhead

    It should be noted that both old-style innerHTML and createContextualFragment are usually susceptible to security issues because event handler attributes will be executed.

    var userinput = "";
    document.createElement('div').innerHTML = userinput;
    document.createRange().createContextualFragment('userinput');
    // both will alert(42)
    

    The exception to innerHTML if the element is a template element.
    DOMParser is also safe.

    var userinput = "";
    new DOMParser.parseFromString(userinput);
    document.createElement('template').innerHTML = userinput;
    // HTMLTemplateElement.content is a DocumentFragment
    

    Theoretically I think it should be possible to use createContextualFragment safely if you get the “context node” to be a template, but I couldn’t figure out how to do it (the API is very confusing)

    • J. Redhead

      ack, the sanitizer ate my html string:

      var userinput = "<img src='x' onerror='alert(42)' />";
  13. Andrew Cottrell

    createContextualFragment may be safely used (Chrome, Firefox 4+, MSIE/EdgeHTML 11+, Opera 12.1+, and Safari 9+) with the following approach

    var userinput = "";
    var docFrag = document.implementation.createHTMLDocument("").createRange().createContextualFragment(userinput);

    Obviously, you’d want to sanitize the docFrag before further use.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!