有没有办法从一般的HTML片段创建文档片段?

时间:2014-03-24 21:00:59

标签: javascript html

我正在开发一个使用一些客户端模板来呈现数据的应用程序,并且大多数javascript模板引擎返回带有标记的简单字符串,并由开发人员将该字符串插入DOM中。

我google了一下,看到一群人建议使用空div,将innerHTML设置为新字符串,然后迭代遍历该div的子节点,如此

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);
}

TADA,documentFragment现在包含已解析的模板。但是,如果我的模板恰好是tr,则在其周围添加div不会达到预期的行为,因为它会在行内添加td的内容。

有人知道解决这个问题的好方法吗?现在我正在检查将插入解析模板的节点,并从其标签名称创建一个元素。我甚至不确定是否有另一种方法可以做到这一点。

在搜索时,我在w3邮件列表上遇到了this discussion,但遗憾的是没有有用的解决方案。

4 个答案:

答案 0 :(得分:5)

您可以将DOMParser用作XHTML,以避免HTML“自动更正”DOM执行:

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>"

这与使用带有临时div的朴素innerHTML形成对比:

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)

通过将模板视为XHTML / XML(确保其格式正确),我们可以改变HTML的常规规则。 DOMParser的覆盖范围应该与对documentFragment的支持相关联,但是在firefox的一些旧拷贝(单位数版本)上,您可能需要使用importNode()。

作为可重复使用的功能:

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;
}

答案 1 :(得分:2)

这是未来的一个,而不是现在,但HTML5定义了一个<template>元素,它将为您创建片段。你将能够:

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

目前适用于Firefox。 See here

答案 2 :(得分:1)

理想的方法是使用HTML5中的<template>标记。您可以以编程方式创建模板元素,为其分配.innerHTML,并且template.content属性中将存在所有已解析的元素(甚至是表的片段)。这可以为您完成所有工作。但是,这只存在于最新版本的Firefox和Chrome中。

如果存在模板支持,它就像这样简单:

function makeDocFragment(htmlString) {
    var container = document.createElement("template");
    container.innerHTML = htmlString;
    return container.content;
}  

此返回结果就像documentFragment一样。您可以直接附加它,它就像documentFragment一样解决问题,除了它具有支持.innerHTML赋值的优势,它允许您使用部分形成的HTML片段(解决我们需要的两个问题)

但是,模板支持在任何地方都不存在,因此您需要一种后备方法。处理回退的强力方法是查看HTML字符串的开头并查看它开始的选项卡类型,并为该类型的标记创建适当的容器,并使用该容器将HTML分配给。这是一种蛮力方法,但它有效。任何类型的HTML元素都需要这种特殊处理,这些HTML元素只能合法地存在于特定类型的容器中。我在下面的代码中包含了一堆这些类型的元素(虽然我没有尝试使列表详尽无遗)。这是代码和下面的工作jsFiddle链接。如果您使用最新版本的Chrome或Firefox,则代码将采用使用模板对象的路径。如果是其他浏览器,它会创建相应类型的容器对象。

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);

包含多个测试用例的工作演示:http://jsfiddle.net/jfriend00/SycL6/

答案 3 :(得分:0)

使用此功能

  • 支持IE11
  • 不得符合xml标准,例如: '&lt; td hidden&gt; 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;
}

@dandavis的解决方案只接受ie11中符合xml的内容。
我不知道是否还有其他标签必须加以考虑?