Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any way to create a document fragment from a generic piece of HTML?

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.

like image 813
Felipe Ruiz Avatar asked Mar 24 '14 21:03

Felipe Ruiz


People also ask

How do you create a fragment in HTML?

To create a new HTML Fragment: Paste your HTML code into the large HTML Code box. Choose between the 'Manual', 'Head' and 'Body' types. Click the Add HTML Fragment button.

What is an HTML document fragment?

The DocumentFragment interface represents a minimal document object that has no parent. It is used as a lightweight version of Document that stores a segment of a document structure comprised of nodes just like a standard document.

What is purpose of method create document fragments?

HTML DOM Document createDocumentFragment() The offscreen node can be used to build a new document fragment that can be insert into any document. The createDocumentFragment() method can also be used to extract parts of a document, change, add, or delete some of the content, and insert it back to the document.

What is createContextualFragment?

createContextualFragment() method returns a DocumentFragment by invoking the HTML fragment parsing algorithm or the XML fragment parsing algorithm with the start of the range (the parent of the selected node) as the context node.


3 Answers

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;
}
like image 163
dandavis Avatar answered Oct 21 '22 00:10

dandavis


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

like image 2
Alohci Avatar answered Oct 21 '22 02:10

Alohci


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/

like image 1
jfriend00 Avatar answered Oct 21 '22 02:10

jfriend00