The ideal approach is to use the <template>
tag from HTML5. You can create a template element programmatically, assign the .innerHTML
to it and all the parsed elements (even fragments of a table) will be present in the template.content
property. This does all the work for you. But, this only exists right now in the latest versions of Firefox and Chrome.
If template support exists, it as simple as this:
function makeDocFragment(htmlString) {
var container = document.createElement("template");
container.innerHTML = htmlString;
return container.content;
}
The return result from this works just like a documentFragment
. You can just append it directly and it solves the problem just like a documentFragment
would except it has the advantage of supporting .innerHTML
assignment and it lets you use partially formed pieces of HTML (solving both problems we need).
But, template support doesn't exist everywhere yet, so you need a fallback approach. The brute force way to handle the fallback is to peek at the beginning of the HTML string and see what type of tab it starts with and create the appropriate container for that type of tag and use that container to assign the HTML to. This is kind of a brute force approach, but it works. This special handling is needed for any type of HTML element that can only legally exist in a particular type of container. I've included a bunch of those types of elements in my code below (though I've not attempted to make the list exhaustive). Here's the code and a working jsFiddle link below. If you use a recent version of Chrome or Firefox, the code will take the path that uses the template object. If some other browser, it will create the appropriate type of container object.
var makeDocFragment = (function() {
// static data in closure so it only has to be parsed once
var specials = {
td: {
parentElement: "table",
starterHTML: "<tbody><tr class='xx_Root_'></tr></tbody>"
},
tr: {
parentElement: "table",
starterHTML: "<tbody class='xx_Root_'></tbody>"
},
thead: {
parentElement: "table",
starterHTML: "<tbody class='xx_Root_'></tbody>"
},
caption: {
parentElement: "table",
starterHTML: "<tbody class='xx_Root_'></tbody>"
},
li: {
parentElement: "ul",
},
dd: {
parentElement: "dl",
},
dt: {
parentElement: "dl",
},
optgroup: {
parentElement: "select",
},
option: {
parentElement: "select",
}
};
// feature detect template tag support and use simpler path if so
// testing for the content property is suggested by MDN
var testTemplate = document.createElement("template");
if ("content" in testTemplate) {
return function(htmlString) {
var container = document.createElement("template");
container.innerHTML = htmlString;
return container.content;
}
} else {
return function(htmlString) {
var specialInfo, container, root, tagMatch,
documentFragment;
// can't use template tag, so lets mini-parse the first HTML tag
// to discern if it needs a special container
tagMatch = htmlString.match(/^\s*<([^>\s]+)/);
if (tagMatch) {
specialInfo = specials[tagMatch[1].toLowerCase()];
if (specialInfo) {
container = document.createElement(specialInfo.parentElement);
if (specialInfo.starterHTML) {
container.innerHTML = specialInfo.starterHTML;
}
root = container.querySelector(".xx_Root_");
if (!root) {
root = container;
}
root.innerHTML = htmlString;
}
}
if (!container) {
container = document.createElement("div");
container.innerHTML = htmlString;
root = container;
}
documentFragment = document.createDocumentFragment();
// start at the actual root we want
while (root.firstChild) {
documentFragment.appendChild(root.firstChild);
}
return documentFragment;
}
}
// don't let the feature test template object hang around in closure
testTemplate = null;
})();
// test cases
var frag = makeDocFragment("<tr><td>Three</td><td>Four</td></tr>");
document.getElementById("myTableBody").appendChild(frag);
frag = makeDocFragment("<td>Zero</td><td>Zero</td>");
document.getElementById("emptyRow").appendChild(frag);
frag = makeDocFragment("<li>Two</li><li>Three</li>");
document.getElementById("myUL").appendChild(frag);
frag = makeDocFragment("<option>Second Option</option><option>Third Option</option>");
document.getElementById("mySelect").appendChild(frag);
Working demo with several test cases: http://jsfiddle.net/jfriend00/SycL6/