shonen.hateblo.jp

やったこと,しらべたことを書く.

html5のtemplateタグは、ただ非表示にする普通のタグでは無い

最近記事を書いていなかったので、書き殴った。

templateタグ

HTML のコンテンツテンプレート (<template>) 要素 は、すなわちページの読み込み時にすぐには描画されないものの、後で JavaScript を使用してインスタンスを生成できる HTML を保持するメカニズムです。

developer.mozilla.org

背景

例えば、javascriptで可変長のレコードを取得して、htmlに反映させることを考える。

こんな風に

<table id="mytable" border="1">
</table>

<script>
function append(items){
    const list = document.getElementById('mytable');
    for (let item of items) {
        const row = document.createElement('tr');
        const elem1 = document.createElement('td');
        elem1.appendChild(document.createTextNode(item));
        row.appendChild(elem1);
        const elem2 = document.createElement('td');
        elem2.appendChild(document.createTextNode(item.length));
        row.appendChild(elem2);
        list.appendChild(row);
    }
}

document.addEventListener('DOMContentLoaded', ()=> {
    append(['foo', 'fizz', 'a']);
});
</script>

javascriptでtrやtdは書きたくない。こういったドキュメントの構成に関する事柄はhtmlに書くべきで、javascriptに書くべきではない。
型となる行をhtmlに書いておき、複製して使い回せばこの問題は回避できる。

<table id="mytable" border="1">
    <tr id="tmpl">
        <td class="str"></td>
        <td class="length"></td>
    </tr>
</table>
<script>
function append(items){
    const list = document.getElementById('mytable');
    const tmpl = document.getElementById('tmpl');
    for (let item of items) {
        const row = tmpl.cloneNode(true);
        const elem1 = row.getElementsByClassName('str')[0];
        elem1.appendChild(document.createTextNode(item));
        const elem2 = row.getElementsByClassName('length')[0];
        elem2.appendChild(document.createTextNode(item.length));
        list.appendChild(row);
    }
}

document.addEventListener('DOMContentLoaded', ()=> {
    append(['foo', 'fizz', 'a']);
});

</script>

上記では型となるtmplが残ってしまう。cssを使って非表示にしたり、tmpl.remove(); したりすれば見えなくすることは出来る。

html5には、このようなdomの使い方をする人のために、templateタグが用意されている。 以下のように使う。

<table id="mytable" border="1">
   <template id="tmpl">
        <tr>
            <td class="str"></td>
            <td class="length"></td>
        </tr>
    </template>
</table>

「templateの子要素にtrが来ているから、そこだけに注意して先程のjavascriptを修正すればええんやな…」
と考えると死ぬ、というのが本記事のテーマである(前置きが長かった)

templateタグを使う

「templateの子要素にtrが来ているから、そこだけに注意して先程のjavascriptを修正すればええんやな…」

を実際にやってみる。 先程はtableで説明していたけれども、事情がありdivに置き換えた。

<div id="mytable">
   <div id="tmpl">
        <div>
            <span class="str"></span>
            <span class="length"></span>
        </div>
    </div>
</div>
<script>
function append(items){
    const list = document.getElementById('mytable');
    const tmplblock = document.getElementById('tmpl');
    const tmpl = tmplblock.children[0];
    for (let item of items) {
        const row = tmpl.cloneNode(true);
        const elem1 = row.getElementsByClassName('str')[0];
        elem1.appendChild(document.createTextNode(item));
        const elem2 = row.getElementsByClassName('length')[0];
        elem2.appendChild(document.createTextNode(item.length));
        list.appendChild(row);
    }
    tmplblock.remove();
}

document.addEventListener('DOMContentLoaded', ()=> {
    append(['foo', 'fizz', 'a']);
});

</script>

問題なく動くはず。

では、id="tmpl"のタグを templateに置き換える。

<div id="mytable">
   <template id="tmpl">
        <div>
            <span class="str"></span>
            <span class="length"></span>
        </div>
    </template>
</div>
<script>
function append(items){
    const list = document.getElementById('mytable');
    const tmplblock = document.getElementById('tmpl');
    const tmpl = tmplblock.children[0];
    for (let item of items) {
        const row = tmpl.cloneNode(true);
        const elem1 = row.getElementsByClassName('str')[0];
        elem1.appendChild(document.createTextNode(item));
        const elem2 = row.getElementsByClassName('length')[0];
        elem2.appendChild(document.createTextNode(item.length));
        list.appendChild(row);
    }
    tmplblock.remove();
}

document.addEventListener('DOMContentLoaded', ()=> {
    append(['foo', 'fizz', 'a']);
});

</script>

これは、「Uncaught TypeError: Cannot read property 'cloneNode' of undefined」で落ちる。
解析してみると、tmplblock.children が空になってしまっているのが分かる。
templateの中身をツリー自体から除外することによって、templateタグの中身を読み込ませないようにしているようである。

templateタグの中身を取り出す

どうやって取り出すのかと言うと、developer.mozilla.orgのサンプル通り、document.importNode を使う。

    // const tmpl = tmplblock.children[0];
    const tmpl = document.importNode(tmplblock.content, true);

改めて実行してみると、「Uncaught TypeError: row.getElementsByClassName is not a function」が発生する。

document-fragment

document.importNode でimportしたオブジェクトは、Elementでは無く、DocumentFragmentという、ドキュメントの断片になる。

developer.mozilla.org

で、眺めていくと、以下に気づく。
ElementやDocumentにはgetElementsByClassName が実装されているが、DocumentFragmentにはgetElementsByClassName は実装されていない。

何で?
そして何故か、querySelector が使える。これを使うしか無い。

<script>
function append(items){
    const list = document.getElementById('mytable');
    const tmplblock = document.getElementById('tmpl');
    const tmpl = document.importNode(tmplblock.content, true);
    for (let item of items) {
        const row = tmpl.cloneNode(true);
        const elem1 = row.querySelector('.str');
        elem1.appendChild(document.createTextNode(item));
        const elem2 = row.querySelector('.length');
        elem2.appendChild(document.createTextNode(item.length));
        list.appendChild(row);
    }
    tmplblock.remove();
}

document.addEventListener('DOMContentLoaded', ()=> {
    append(['foo', 'fizz', 'a']);
});
</script>

これで動いた。 ところで、document.importNodeは参照ではなくクローンなので、cloneNode する必要は無い。よって、

    // const tmpl = document.importNode(tmplblock.content, true);
    for (let item of items) {
        // const row = tmpl.cloneNode(true);
        const row = document.importNode(tmplblock.content, true);

成果物

最終的なコードは、

<div id="mytable">
   <template id="tmpl">
        <div>
            <span class="str"></span>
            <span class="length"></span>
        </div>
    </template>
</div>
<script>
function append(items){
    const list = document.getElementById('mytable');
    const tmplblock = document.getElementById('tmpl');
    for (let item of items) {
        const row = document.importNode(tmplblock.content, true);
        const elem1 = row.querySelector('.str');
        elem1.appendChild(document.createTextNode(item));
        const elem2 = row.querySelector('.length');
        elem2.appendChild(document.createTextNode(item.length));
        list.appendChild(row);
    }
    tmplblock.remove();
}

document.addEventListener('DOMContentLoaded', ()=> {
    append(['foo', 'fizz', 'a']);
});

</script>

おまけ

tableタグの罠

解説のため、以下のようなhtmlを書いた。

templateをdivに置き換えただけ。

<table id="mytable" border="1">
   <div id="tmpl">
        <tr>
            <td class="str"></td>
            <td class="length"></td>
        </tr>
    </div>
</table>
<script>
function append(items){
    const list = document.getElementById('mytable');
    const tmplblock = document.getElementById('tmpl');
    const tmpl = tmplblock.children[0];
    for (let item of items) {
        const row = tmpl.cloneNode(true);
        const elem1 = row.getElementsByClassName('str')[0];
        elem1.appendChild(document.createTextNode(item));
        const elem2 = row.getElementsByClassName('length')[0];
        elem2.appendChild(document.createTextNode(item.length));
        list.appendChild(row);
    }
    tmplblock.remove();
}

document.addEventListener('DOMContentLoaded', ()=> {
    append(['foo', 'fizz', 'a']);
});

</script>

実行すると、tmplblock.children が空になって、後続のコードが失敗する。
tableの中にdivのような、良からぬタグが入っていると消されるらしい?