Revealing the magic: How to properly convert HTML string to a DOM element
This seems like a trivial task. However, it is not. There are few popular solutions, but they have a big problem and if you use them a lot you will spot the issue.
Let’s say that we have the following HTML markup:
If you google the problem you will find that the most popular solution involves the usage of .innerHTML property of a newly created element.
The result of the above script is actually correct. You will get section element with link inside it. Everything seems ok and it looks like you did the trick. Now let’s try with something else:
console.log(notWorking('
Text Here
'));
«When one sets the innerHTML property of an element, the string containing the HTML is run through the parser.»
console.log(notWorking('
Text Here
'));
Which proves the words from Eric Vasilik. My method should somehow examine the passed HTMl and automatically adds the necessary tags. Along with that it should return not the first child node, but the correct nested element. Looks like a lot of work.
Ok, but . wait a minute. jQuery doesn’t have this problem. You may pass
Text Here
and still get the desired result. I started digging into the jQuery’s code and found this:
// We have to close these tags to support XHTML (#13200) wrapMap = < option: [ 1, "" ], legend: [ 1, "" ], area: [ 1, "" ], param: [ 1, "" ], thead: [ 1, "
", "
" ], tr: [ 2, "
", "
" ], col: [ 2, "
", "
" ], td: [ 3, "
", "
" ], // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, // unless wrapped in a div with non-breaking characters in front of it. _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
", "
" ] >,
This looks like the magic which solves the problem. I cheated a bit and got the code above. I managed to extract that functionality into a single method:
var str2DOMElement = function(html) < /* code taken from jQuery */ var wrapMap = < option: [ 1, "" ], legend: [ 1, "" ], area: [ 1, "" ], param: [ 1, "" ], thead: [ 1, "
", "
" ], tr: [ 2, "
", "
" ], col: [ 2, "
", "
" ], td: [ 3, "
", "
" ], // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, // unless wrapped in a div with non-breaking characters in front of it. _default: [ 1, "
", "
" ] >; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; var element = document.createElement('div'); var match = //g.exec(html); if(match != null) < var tag = match[0].replace(//g, ''); var map = wrapMap[tag] || wrapMap._default, element; html = map[1] + html + map[2]; element.innerHTML = html; // Descend through wrappers to the right content var j = map[0]+1; while(j--) < element = element.lastChild; >> else < // if only text is passed element.innerHTML = html; element = element.lastChild; >return element; >
The map of jQuery nicely shows me what exactly I should wrap my string in. There are few lines of code which find the root tag and its type. At the beginning I wondered what are this numbers in the wrapMap object for. Later I found this:
That was the code that returns the needed DOM element from the build tree. And the numbers were the level of nesting. Pretty simple, but I think, one of the most used features of jQuery.
There is a special case when you want to create a new body tag. The function above doesn’t work because the div element could not have a body inside. Here is the fixed version.
" ] >; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; var match = //g.exec(html); var element = document.createElement('div'); if(match != null) < var tag = match[0].replace(//g, '').split(' ')[0]; if(tag.toLowerCase() === 'body') < var dom = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null); var body = document.createElement("body"); // keeping the attributes element.innerHTML = html.replace(//g, '
'); var attrs = element.firstChild.attributes; body.innerHTML = html; for(var i=0; i return body; > else < var map = wrapMap[tag] || wrapMap._default, element; html = map[1] + html + map[2]; element.innerHTML = html; // Descend through wrappers to the right content var j = map[0]+1; while(j--) < element = element.lastChild; >> > else < element.innerHTML = html; element = element.lastChild; >return element; >
Just recently, I released my first online video course on advanced JavaScript called «Do you speak JavaScript?». Check it out here. v8.0.22 | Driven by Lumina CMS
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.
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:
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 =``;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 =``;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.
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() — winner
Element.innerHTML — winner
DOMParser.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:
“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).”
MDN on DocumentFragment
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.
We welcome your feedback
We enjoy compliments, but you can totally shout at us for doing it wrong on our Twitter account 👋
Interested in our work? Visit our regular website or check us out on GitHub and Twitter.
In an HTML document, the document.createElement() method creates the HTML element specified by tagName , or an HTMLUnknownElement if tagName isn’t recognized.
Syntax
let element = document.createElement(tagName[, options]);
Parameters
tagName A string that specifies the type of element to be created. The nodeName of the created element is initialized with the value of tagName . Don’t use qualified names (like «html:a») with this method. When called on an HTML document, createElement() converts tagName to lower case before creating the element. In Firefox, Opera, and Chrome, createElement(null) works like createElement(«null») . options Optional An optional ElementCreationOptions object, containing a single property named is , whose value is the tag name of a custom element previously defined via customElements.define() . See Web component example for more details.
Return value
Examples
Basic example
This creates a new and inserts it before the element with the ID » div1 «.
HTML
The text above has been created dynamically.
JavaScript
document.body.onload = addElement; function addElement () < // create a new div element const newDiv = document.createElement("div"); // and give it some content const newContent = document.createTextNode("Hi there and greetings!"); // add the text node to the newly created div newDiv.appendChild(newContent); // add the newly created element and its content into the DOM const currentDiv = document.getElementById("div1"); document.body.insertBefore(newDiv, currentDiv); >
Web component example
The following example snippet is taken from our expanding-list-web-component example (see it live also). In this case, our custom element extends the HTMLUListElement , which represents the element.
// Create a class for the element class ExpandingList extends HTMLUListElement < constructor() < // Always call super first in constructor super(); // constructor definition left out for brevity . >> // Define the new element customElements.define('expanding-list', ExpandingList, < extends: "ul" >);
If we wanted to create an instance of this element programmatically, we’d use a call along the following lines:
let expandingList = document.createElement('ul', < is : 'expanding-list' >)
The new element will be given an is attribute whose value is the custom element’s tag name.
Note: For backwards compatibility with previous versions of the Custom Elements specification, some browsers will allow you to pass a string here instead of an object, where the string’s value is the custom element’s tag name.
Specifications
Browser compatibility
The compatibility table on this page is generated from structured data. If you’d like to contribute to the data, please check out https://github.com/mdn/browser-compat-data and send us a pull request.