Create a DOM node from an HTML string
There are many different ways to convert a string of HTML to a DOM node/element. Here's a comparison of common methods, including caveats and things to consider.
Methods
We’re asuming that the used html
contains a string with valid HTML.
innerHTML
const placeholder = document.createElement("div");
placeholder.innerHTML = html;
const node = placeholder.firstElementChild;
- Safety: no script execution (see caveats)
- Allowed nodes: only valid nodes (see HTML restrictions)
- Support: 👍👍👍
- MDN: Element.innerHTML
insertAdjacentHTML
const placeholder = document.createElement("div");
placeholder.insertAdjacentHTML("afterbegin", html);
const node = placeholder.firstElementChild;
- Safety: no script execution (see caveats)
- Allowed nodes: only valid nodes (see HTML restrictions)
- Support: 👍👍👍 (but below IE 10 there’s no support for
table
-related nodes) - MDN: Element.insertAdjacentHTML()
DOMParser
const node = new DOMParser().parseFromString(html, "text/html").body
.firstElementChild;
- Safety: no script execution (see caveats)
- Allowed nodes: only valid nodes (see HTML restrictions)
- Support: 👍👍 (IE 10+, Safari 9.1+)
- MDN: DOMParser
Range
const node = document.createRange().createContextualFragment(html);
- Safety: executes scripts, sanitize first 🚨
- Allowed nodes: you can set context (see HTML restrictions)
- Support: 👍 (IE 10+, Safari 9+)
- MDN: Range.createContextualFragment()
Note: in most examples we’re using firstElementChild, since this will prevent you having to trim
any whitespace (as opposed to firstChild
). Note that this is not supported in IE and Safari when a DocumentFragment is returned. In our case that’s not a problem since the fragment itself is the node we want.
Caveats
There are a few things to consider when choosing a method. Will it handle user generated content? Do we need to support table
-related nodes?
HTML restrictions
There are a few restrictions in HTML which will prevent adding certain types of nodes to a node like div
, think of thead
, tbody
, tr
and td
.
Most methods will return null
when you try to create one of these nodes:
const placeholder = document.createElement("div");
placeholder.innerHTML = `<tr><td>Foo</td></tr>`;
const node = placeholder.firstElementChild; //=> null
createContextualFragment
With createContextualFragment
you can circumvent this by setting the context, as Jake Archibald pointed out:
const table = document.createElement(`table`);
const tbody = document.createElement(`tbody`);
table.appendChild(tbody);
const range = document.createRange();
range.selectNodeContents(tbody);
const node = range.createContextualFragment(`<tr><td>Foo</td></tr>`); //=> tr
template
Another way is by using a template tag as the placeholder, which doesn’t have any content restrictions:
const template = document.createElement("template");
template.innerHTML = `<tr><td>Foo</td></tr>`;
const node = template.content.firstElementChild; //=> tr
Note that template
is not supported in any IE version.
Alternatives
You could also opt for a solution using DocumentFragment, or make the temporary placeholder you’re appending to a table
. The latter will return a tbody
as well.
Script execution
All methods except createContextualFragment
will prevent ‘regular script execution’:
const placeholder = document.createElement("div");
placeholder.innerHTML = `<div><script>alert('Foo');</script></div>`;
const node = placeholder.firstElementChild;
document.body.appendChild(node); //=> will not show an alert
There are, however, ways to execute scripts without script
tags (see MDN):
const placeholder = document.createElement("div");
placeholder.innerHTML = `<img src='x' onerror='alert(1)'>`;
const node = placeholder.firstElementChild;
document.body.appendChild(node); //=> will show an alert (!)
Note that the above won’t throw an alert
in Firefox, but it does so in Chrome.
Sanitizing
You could strip all offending attributes of child nodes before appending the actual node to the DOM, although there are probably other issues that you should be aware of.
[...placeholder.querySelectorAll("*")].forEach((node) =>
node.removeAttribute("onerror")
);
Key takeaway: if you’re parsing user-generated content, make sure to sanitize properly.
Performance
Unless you’re adding a huge amount of nodes to your page, performance shouldn’t be a big problem with any of these methods. Here are the results of multiple runs of a jsPerf benchmark, which ran in the latest versions of Chrome and Firefox:
Range.createContextualFragment()
— winner (fastest in Firefox)Element.insertAdjacentHTML()
— winnerElement.innerHTML
— winnerDOMParser.parseFromString()
— 90% slower
Note that results differ from test to test. However, the clear ’loser’ appears to be DOMParser
.
Further improvements
When adding multiple nodes at once, it is recommended to use a DocumentFragment as placeholder and append all nodes at once:
const htmlToElement = (html) => ({
/* ... */
});
const fragment = document.createDocumentFragment();
items.forEach((item) => {
const node = htmlToElement(`<div>${item.name}</div>`);
fragment.appendChild(node);
});
document.body.appendChild(fragment);
This will cause only one reflow:
“Since the document fragment is in memory and not part of the main DOM tree, appending children to it does not cause page reflow (computation of element’s position and geometry).”
Conclusion
There are different ways to get the desired outcome. Maybe we’ve missed some. There’s no ideal way or ‘best solution’, so choose what’s working for you.
For us: we’ve been using the innerHTML
option in our own @grrr/utils library (see the htmlToElement function). This has been largely due to its browser support. We’d probably move to a template
version in the future when IE-support isn’t an issue anymore.
Updated on June 14, 2019: Increased IE support for Range
after a conversation on Twitter.