12

I'm working on an application that uses some client-side templates to render data and most of the javascript template engines return a simple string with the markup and it's up to the developer to insert that string into the DOM.

I googled around and saw a bunch of people suggesting the use of an empty div, setting its innerHTML to the new string and then iterating through the child nodes of that div like so

var parsedTemplate = 'something returned by template engine';
var tempDiv = document.createElement('div'), childNode;
var documentFragment = document.createDocumentFragment;
tempDiv.innerHTML = parsedTemplate;
while ( childNode = tempDiv.firstChild ) {
    documentFragment.appendChild(childNode);
}

And TADA, documentFragment now contains the parsed template. However, if my template happens to be a tr, adding the div around it doesn't achieve the expected behaviour, as it adds the contents of the td's inside the row.

Does anybody know of a good way of to solve this? Right now I'm checking the node where the parsed template will be inserted and creating an element from its tag name. I'm not even sure there's another way of doing this.

While searching I came across this discussion on the w3 mailing lists, but there was no useful solution, unfortunately.

Felipe Ruiz
  • 181
  • 1
  • 6
  • in your code sample the temp div is never appended to the document fragment – Jeremy Danyow Mar 24 '14 at 21:31
  • @jdanyow: the children of the temp div are append in the while loop... – dandavis Mar 24 '14 at 21:32
  • You can't put incomplete HTML like `text` into any DOM element by itself. If that's the template piece you're dealing with, then you will have to see a whole different approach. If you know it's a table row, then you can insert it in a placedholder table element via `.innerHTML` and then extract out the rows that get created in the placeholder `` element, but you would have to special cause things that must be in a special type of container such as ``, `
    `, `
  • `, etc...
  • – jfriend00 Mar 24 '14 at 21:39
  • @jfriend00: you cannot put that in any DOM element, but you can put it in a fragment... – dandavis Mar 24 '14 at 21:49

4 Answers4

5

You can use a DOMParser as XHTML to avoid the HTML "auto-correction" DOMs like to perform:

var parser = new DOMParser(),
doc = parser.parseFromString('<tr><td>something returned </td><td>by template engine</td></tr>', "text/xml"),
documentFragment = document.createDocumentFragment() ;
documentFragment.appendChild( doc.documentElement );

//fragment populated, now view as HTML to verify fragment contains what's expected:
var temp=document.createElement('div');
temp.appendChild(documentFragment);
console.log(temp.outerHTML); 
    // shows: "<div><tr><td>something returned </td><td>by template engine</td></tr></div>"

this is contrasted to using naive innerHTML with a temp div:

var temp=document.createElement('div');
temp.innerHTML='<tr><td>something returned </td><td>by template engine</td></tr>';
console.log(temp.outerHTML); 
// shows: '<div>something returned by template engine</div>' (bad)

by treating the template as XHTML/XML (making sure it's well-formed), we can bend the normal rules of HTML. the coverage of DOMParser should correlate with the support for documentFragment, but on some old copies (single-digit-versions) of firefox, you might need to use importNode().

as a re-usable function:

function strToFrag(strHTML){
   var temp=document.createElement('template');
    if( temp.content ){
       temp.innerHTML=strHTML;
       return temp.content;
    }
    var parser = new DOMParser(),
    doc = parser.parseFromString(strHTML, "text/xml"),
    documentFragment = document.createDocumentFragment() ;
    documentFragment.appendChild( doc.documentElement );
    return documentFragment;
}
dandavis
  • 16,370
  • 5
  • 40
  • 36
  • 1
    @jfriend00: one can just (almost magically) appendChild() the documentFragment into the . that and high performance are the main reasons to use a fragment instead of a stand-alone div... – dandavis Mar 24 '14 at 21:44
  • What is the browser support for using the `DOMParser()` interface with HTML? MDN (my usual source for that type of info) isn't very clear and makes it sound like there might be issues in Safari and perhaps earlier versions of IE. – jfriend00 Mar 24 '14 at 23:23
  • So, you're telling it you're giving it `"text/xml"` and that's why it's working in the browsers that don't claim to support `"text/html"`? Are there any gotchas with that? – jfriend00 Mar 25 '14 at 00:10
  • 2
    This seems to be broken in all modern browsers. The elements parsed as text/xml are XML elements, not HTML elements. They show up in the DOM tree, but they get rendered without any semantics as if they are all span elements. So a `` inserted in table like this will get its content rendered as plain text. – Robert Važan Nov 27 '15 at 11:22
  • 1
    modern browsers should be using the – dandavis Nov 27 '15 at 12:58
2

This is one for the future, rather than now, but HTML5 defines a <template> element that will create the fragment for you. You will be able to do:

var parsedTemplate = '<tr><td>xxx</td></tr>';
var tempEL = document.createElement('template');
tempEl.innerHTML = parsedTemplate;
var documentFragment = tempEl.content;

It currently works in Firefox. See here

Alohci
  • 78,296
  • 16
  • 112
  • 156
  • The HTML5 template is perfect for this (when support is present). It is supported in the latest versions of Chrome and Firefox, but not in earlier versions of those or any version of IE though. – jfriend00 Mar 24 '14 at 23:24
  • 1
  • 1
    `template.content` (if template tag support exists) works just like a fragment. See the code and demo in my answer for a working version. If you run the jsFiddle in my answer in Chrome or Firefox, it only takes about 5 lines of code using the template tag to solve the problem. – jfriend00 Mar 24 '14 at 23:31
  • 1
    ahh, i see the intrinsic fragment there now... DOMParser is about as much boiler plate, so i don't see a huge advantage. I thought template tag's big thing was not pinging images and scripts when shipping templated HTML inside a web app's document, but you do point out an interesting side-effect; kudos. – dandavis Mar 24 '14 at 23:37
1

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/

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • it seems makeDocFragment('something returned by template engine') throws away the table tags in the same way the OP wanted fixed... – dandavis Mar 24 '14 at 21:34
  • @dandavis - You can't use incomplete HTML strings like `something returned by template engine`. That isn't a valid piece of HTML that can exist on it's own, in a div or in a fragment. If that's the real issue here, then the OP needs a completely different approach. They can't use a fragment to hold the incomplete, but parsed HTML. The browser doesn't support that. – jfriend00 Mar 24 '14 at 21:36
  • Completely rethought how to do this by checking to see what type of initial tag is in the piece of HTML and creating the right type of container node to hold that type of tag. – jfriend00 Mar 24 '14 at 22:28
  • Added support for the ` – jfriend00 Mar 24 '14 at 22:50
  • @dandavis - `template.content` works just like a `documentFragment`. See the simplified example I added to the beginning of my answer that shows how simple it all is with a template. – jfriend00 Mar 24 '14 at 23:34
  • yeah, i see that. now. i played with a fragment trying to answer this, but i overlooked the content property. kudos on a comprehensive solution. – dandavis Mar 24 '14 at 23:47
0

Use this function

  • supports IE11
  • has not to be xml-conform e.g. '<td hidden>test'

function createFragment(html){
    var tmpl = document.createElement('template');
    tmpl.innerHTML = html;
    if (tmpl.content == void 0){ // ie11
        var fragment = document.createDocumentFragment();
        var isTableEl = /^[^\S]*?<(t(?:head|body|foot|r|d|h))/i.test(html);
        tmpl.innerHTML = isTableEl ? '<table>'+html : html;
        var els        = isTableEl ? tmpl.querySelector(RegExp.$1).parentNode.childNodes : tmpl.childNodes;
        while(els[0]) fragment.appendChild(els[0]);
        return fragment;
    }
    return tmpl.content;
}

The solution from @dandavis will accept only xml-conform content in ie11.
I dont know if there are other tag which must be taken into account?

Tobias Buschor
  • 3,075
  • 1
  • 22
  • 22