html5のtemplateタグは、ただ非表示にする普通のタグでは無い
最近記事を書いていなかったので、書き殴った。
templateタグ
HTML のコンテンツテンプレート (<template>) 要素 は、すなわちページの読み込み時にすぐには描画されないものの、後で JavaScript を使用してインスタンスを生成できる HTML を保持するメカニズムです。
背景
例えば、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という、ドキュメントの断片になる。
で、眺めていくと、以下に気づく。
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のような、良からぬタグが入っていると消されるらしい?