You can achieve without having to use document.createElement()
as I've seen in some of the comments here, and without wrapping everything in some parent element like <template>
as I also see in some solutions.
To achieve this, we must first understand how the parseFromString() method works. From the first line of the docs, we can see that…
The parseFromString()
method of the DOMParser
interface parses a string containing either HTML or XML, returning an HTMLDocument
or an XMLDocument
.
Here are the requirements for both document types:
HTMLDocument
(text/html
) :: Must be valid HTML (where <tr>
must be the descendant of a <table>
element
XMLDocument
(text/xml
) :: Must have one parent element; cannot have multiple top-level elements
The main issue here lies in fact that the element parsed as text/html
needs to read as valid HTML, which the top-level <tr>
does not since it requires a <table>
ancestor.
Here's the good news— XML is more accepting of "improper" HTML tags since it deals largely with custom tags for data sources. The main downside of XML would normally be that all the elements would not exist in an HTML hierarchy and that you would need one parent element. However, we can take advantage of this by creating the DOM tree the exact way you are wanting to in XML first and then pass all those elements to the new HTMLDocument using appendChild()
and a for...of
loop.
Here it is in action. I've added a function decorator to make this cleaner:
DOMParser.prototype.looseParseFromString = function(str) {
str = str.replace(/ \/>/g, '>').replace(/(<(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr).*?>)/g, '$1</$2>');
const xdom = this.parseFromString('<xml>'+str+'</xml>', 'text/xml');
const hdom = this.parseFromString('', 'text/html');
for (elem of Array.from(xdom.documentElement.children)) {
hdom.body.appendChild(elem);
}
for (elem of Array.from(hdom.querySelectorAll('area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr'))) {
elem.outerHTML = '<'+elem.outerHTML.slice(1).split('<')[0];
}
return hdom;
}
const parser = new DOMParser();
const domString = '<tr> <td>a</td> </tr> <div>X</div> <div><img src="" />Test<br /></div>';
const dom = parser.looseParseFromString(domString);
// I added the below log() function to make the testing experience easier to digest by logging the contents of the output to the document *in addition to* the console, though I've hidden the console to save room. Any of thse should work just the same in your local console.
const printHTML = htmlContent => (typeof htmlContent === "string" ? htmlContent : htmlContent.outerHTML).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
const log = (label, htmlContent, classStr) => (console.log(label, (typeof htmlContent === "string" ? htmlContent : htmlContent.outerHTML)), document.body.insertAdjacentHTML('beforeend', `<div class="log-entry${classStr?" "+classStr:""}" data-label="${label}"><pre>${printHTML(htmlContent)}</pre></div>`));
log("DOM string used for testing", domString, 'title')
log("dom.body", dom.body);
log("dom.querySelector('tr')", dom.querySelector('tr'));
log("dom.querySelector('div')",
dom.querySelector('div'));
log("dom.querySelector('br')", dom.querySelector('br'));
@import url(https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@700&display=swap);body{display:flex;flex-direction:column;font-family:'Source Code Pro',monospace;font-size:13px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.log-entry{display:flex;flex-direction:column;box-sizing:border-box}.log-entry+.log-entry{margin-top:8px}.log-entry::before,.log-entry>pre{padding:8px 16px}.log-entry::before{display:block;width:100%;background-color:#37474f;border-radius:10px 10px 0 0;content:attr(data-label);color:#eceff1;box-sizing:border-box}.log-entry>pre{display:block;margin:0;background-color:#cfd8dc;border-radius:0 0 10px 10px;color:#263238;white-space:break-spaces;box-sizing:border-box}.log-entry.title:first-of-type{position:sticky;top:0;margin:-8px -8px 4px -8px;box-shadow:0 0 30px 0 #263238}.log-entry.title::before{background:#000;border-radius:0;color:#3f3;text-shadow:0 2px 6px rgba(51,255,51,.5)}.log-entry.title>pre{padding-top:0;background:#000;border-radius:0;color:#fff}
The end result should do exactly what you're looking for here and all elements retain all HTML properties and methods, as does the final DOM work exactly as any other HTMLDocument
object.
Moving forward, after initializing this prototype method, you would only need to use this one line to replace the line you mentioned in your original question:
parser.looseParseFromString('<tr> <td>a</td> </tr> <div>X</div>')
UPDATED (2021-05-04 20:22 GMT-0400)
UPDATES |
1. I have updated my looseParseFromString() function to account for void HTML elements, which do not need a closing tag. I gathered this list of void tag names from this article by Lifewire. I worked around this issue by using a regex replacement to close any void tags and replace any XHTML-formatted void tag closures with simple HTML ones (e.g. <br /> ➞ <br></br> ). Once the XMLDocument is successfully constructed, I loop through and create the HTMLDocument as I did before. After that, my function loops back through any of the void elements with closing tags in the new HTMLDocument, using the same list of void tag names from earlier, and removes the closing tags using the outerHTML property and the split() method. |
2. I also implemented two helper functions log() and printHTML() which assist in simplifying the testing process by logging the results to the test window's document.body in addition to the console. I encourage you to test this code in your own console as well. It works the same across both for me. |