JavaScript でリストやカードを何個も生成するとき、HTML 文字列を innerHTML で組み立てていくと、タグの閉じ忘れやエスケープ漏れで壊れやすくなります。<template> 要素を使うと、繰り返したいマークアップを「雛形」として HTML 側に置いておき、JavaScript ではそれを複製して値を差し込むだけで済みます。この記事では、<template> の性質と、content・cloneNode() を組み合わせて配列データから要素を動的に生成する流れを、動くコードで解説します。
目次
template 要素は「描画されない下書き」
<template> は、その中に書いたマークアップをすぐには描画しないための要素です。ページに書いても画面には何も表示されず、レイアウトにも影響しません。中に <img> があっても画像は読み込まれず、<script> があっても実行されません。あくまで「あとで複製して使うための下書き」として、ブラウザが内容を保持してくれるだけの要素だと考えると分かりやすいです。
たとえば、次のように商品カードの雛形を置いておきます。
<template id="card-template">
<div class="card">
<h3 class="card__name"></h3>
<p class="card__price"></p>
</div>
</template>
この状態でブラウザに読み込ませても、カードは表示されません。中身は DOM ツリー上には存在しますが、通常の文書とは切り離された特別な場所に保持されています。その中身にアクセスするための入り口が content プロパティです。
content プロパティで中身の DocumentFragment を取り出す
<template> の中身は、要素オブジェクトの content プロパティから取得できます。このとき返ってくるのは通常の要素ではなく DocumentFragment(ドキュメントフラグメント)というオブジェクトです。DocumentFragment は「複数の要素をまとめて持てる、軽量な入れ物」で、それ自体はページに表示されません。appendChild() で本物の DOM に追加すると、入れ物は消えて中身の子要素だけが挿入される、という性質があります。
<template>・content・cloneNode() のそれぞれが何を指すのかを整理しておきます。
| 要素・プロパティ | 役割 |
|---|---|
<template> | 描画されない雛形マークアップを保持する要素 |
template.content | 雛形の中身を持つ DocumentFragment(読み取り専用) |
cloneNode(true) | ノードを子孫ごと複製する。true で深いコピー |
cloneNode(true) で複製してから追加する
雛形を使い回すときは、content をそのまま追加するのではなく、cloneNode(true) で複製してから使います。content は雛形の実体そのものなので、そのまま appendChild() してしまうと元の雛形が空になり、2 回目以降に複製できなくなるからです。複製を作れば、雛形は元のまま残り、何度でも同じ形を生成できます。
cloneNode() の引数は「子孫まで複製するかどうか」を表します。true を渡すと中の子要素まで含めた深いコピーになります。省略または false にすると、その要素だけをコピーして中身は空になるため、雛形の複製では基本的に true を使います。基本の流れは次のとおりです。
const template = document.getElementById("card-template");
// 中身を子孫ごと複製する
const clone = template.content.cloneNode(true);
// 複製した中の要素に値を差し込む
clone.querySelector(".card__name").textContent = "りんご";
clone.querySelector(".card__price").textContent = "150円";
// 本物の DOM に追加する。ここで初めて表示される
document.getElementById("list").appendChild(clone);
ポイントは、値を書き換える対象が clone(複製した DocumentFragment)の中の要素だということです。appendChild() する前に querySelector() で中の要素を取り出し、テキストや属性を設定しておきます。追加してしまうと clone は空になるので、書き換えは追加の前に行います。
配列データからカードをまとめて生成する
実際のユースケースでは、配列で持ったデータをループで回し、1 件ごとに雛形を複製して差し込む、という書き方をよく使います。下のデモは、メンバーの配列から forEach でカードを生成しています。JavaScript タブと HTML タブを見比べると、雛形が 1 つあれば件数分のカードを組み立てられることが分かります。実行して Preview を確認してみてください。
<!-- カードの雛形。template の中身は最初は描画されない -->
<template id="card-template">
<div class="card">
<span class="card__badge"></span>
<h3 class="card__name"></h3>
<p class="card__desc"></p>
</div>
</template>
<!-- 生成したカードをここに差し込む -->
<div id="list" class="card-list"></div>
body {
font-family: sans-serif;
margin: 16px;
background: #f7f8fa;
}
.card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.card {
background: #fff;
border: 1px solid #e3e6ea;
border-radius: 8px;
padding: 14px 16px;
}
.card__badge {
display: inline-block;
font-size: 12px;
color: #fff;
background: #007bff;
border-radius: 999px;
padding: 2px 10px;
}
.card__name {
margin: 8px 0 4px;
font-size: 16px;
}
.card__desc {
margin: 0;
font-size: 13px;
color: #555;
}
// 表示したいデータの配列
const members = [
{ badge: "PHP", name: "田中", desc: "サーバーサイド担当" },
{ badge: "JS", name: "佐藤", desc: "フロントエンド担当" },
{ badge: "CSS", name: "鈴木", desc: "デザイン担当" },
];
const template = document.getElementById("card-template");
const list = document.getElementById("list");
members.forEach((member) => {
// .content は中身の DocumentFragment。true で子孫ごと複製する
const clone = template.content.cloneNode(true);
// 複製した断片の中を書き換える
clone.querySelector(".card__badge").textContent = member.badge;
clone.querySelector(".card__name").textContent = member.name;
clone.querySelector(".card__desc").textContent = member.desc;
// 断片をまとめて追加。ここで初めて画面に現れる
list.appendChild(clone);
});
ループの中で毎回 cloneNode(true) を呼んでいる点に注目してください。1 回の複製が 1 件分のカードになります。件数が増えても雛形は 1 つのままで、HTML 側とロジック側の役割がきれいに分かれるため、後から項目を足したりデザインを変えたりするのも簡単です。
大量に追加するときは DocumentFragment にまとめる
件数が多いとき、複製するたびに list.appendChild(clone) を実行すると、追加のたびにブラウザが再描画の準備をするため効率が良くありません。そこで、いったん別の DocumentFragment にすべてのカードをためておき、最後に一度だけ本物の DOM へ追加する方法があります。document.createDocumentFragment() で空の入れ物を作り、そこへ複製を足していきます。
const template = document.getElementById("card-template");
const list = document.getElementById("list");
// まとめ用の入れ物を用意する
const fragment = document.createDocumentFragment();
members.forEach((member) => {
const clone = template.content.cloneNode(true);
clone.querySelector(".card__name").textContent = member.name;
fragment.appendChild(clone); // まだ画面には出ない
});
// 最後に一度だけ本物の DOM へ追加する
list.appendChild(fragment);
fragment.appendChild(clone) の時点ではまだ画面に反映されず、最後の list.appendChild(fragment) でまとめて挿入されます。DocumentFragment は追加時に中身だけが差し込まれるので、余計なラッパー要素が増えることもありません。
カードが表示されないときに確認すること
content を付け忘れている
複製するときに template.cloneNode(true) と書いてしまうと、<template> 要素そのものが複製され、追加しても中身は描画されません。複製したいのは中身なので、template.content.cloneNode(true) のように content を経由する必要があります。表示されないときはまずここを確認します。
スクリプトの実行タイミングが早すぎる
getElementById("card-template") が null になる場合、<template> がまだ読み込まれる前に JavaScript が動いています。<script> を </body> の直前に置くか、<script> に defer 属性を付けるか、DOMContentLoaded イベントの中で処理を実行して、要素が揃ってから動かすようにします。
複製せずに content を直接追加している
ループの中で list.appendChild(template.content) のように content を直接追加すると、1 件目は表示されても 2 件目以降が出ません。content の中身は移動してしまい、雛形が空になるためです。必ず cloneNode(true) で複製したものを追加します。
まとめ
<template> は、描画もスクリプト実行もされない「雛形」を HTML に置いておくための要素です。中身は content プロパティから DocumentFragment として取り出し、cloneNode(true) で子孫ごと複製してから値を差し込み、appendChild() で本物の DOM に追加します。配列データをループで回して雛形を複製すれば、HTML 文字列を手で組み立てるより安全に、繰り返しの要素を生成できます。大量に追加するときは DocumentFragment にまとめてから一度に挿入すると再描画を抑えられます。表示されないときは、content を経由しているか、複製してから追加しているか、スクリプトの実行タイミングは適切かを確認してください。