こんにちは、かつコーチです。
前回は querySelector を使って、DOMツリーの中から目的の要素を取得する方法を解説しました。
今回は一歩進んで、DOMツリーそのものに新しい要素を追加したり、不要な要素を削除したりする方法を扱います。
「Todoリストにアイテムを追加する」「通知を消す」といった、実際のWebアプリでよく見る動きの基礎になる部分です。
要素を新しく作る:createElement
基本の書き方
新しい要素をDOMに追加するには、まず document.createElement() で要素そのものを作ります。
const p = document.createElement("p");
p.textContent = "新しく追加した段落です";
console.log(p);
// <p>新しく追加した段落です</p>
ここで注意したいのは、createElement で作った時点では、この要素はまだ画面には表示されていないということです。
作った要素は、いわば「まだどこにも置かれていない部品」のような状態です。
この部品をDOMツリーのどこかに「取り付ける」ことで、初めて画面に表示されます。
appendChildで子要素として追加する
作った要素をDOMツリーに追加するには、appendChild() を使います。
<ul id="fruit-list">
<li>りんご</li>
</ul>
const list = document.querySelector("#fruit-list");
const newItem = document.createElement("li");
newItem.textContent = "みかん";
list.appendChild(newItem);
list.appendChild(newItem) は、「list の子要素の一番最後に newItem を追加する」という意味です。
このコードを実行すると、画面には「りんご」の下に「みかん」が追加された状態で表示されます。
insertBeforeで挿入位置を指定する
appendChild は必ず一番最後に追加されますが、途中に挿入したい場合は insertBefore() を使います。
const list = document.querySelector("#fruit-list");
const referenceItem = document.querySelector("#fruit-list li"); // 最初の<li>
const newItem = document.createElement("li");
newItem.textContent = "ぶどう";
list.insertBefore(newItem, referenceItem);
// 「りんご」の前に「ぶどう」が挿入される
insertBefore(挿入する要素, 基準となる要素) という順番で指定する点に注意してください。
要素を削除する
removeChildによる削除
要素を削除するときは、removeChild() を使う方法が昔からよく使われてきました。
const list = document.querySelector("#fruit-list");
const target = document.querySelector("#fruit-list li"); // 最初の<li>
list.removeChild(target);
removeChild() は「親要素」に対して呼び出し、「どの子要素を削除するか」を引数で指定する、という少し回りくどい書き方になっています。
remove()でシンプルに削除する
現在は、削除したい要素自身に対して remove() を呼び出すだけの、よりシンプルな方法が使えます。
const target = document.querySelector("#fruit-list li"); // 最初の<li>
target.remove();
親要素を経由せず、削除したい要素だけを直接指定できるので、コードの意図も読みやすくなります。
新しくコードを書くのであれば、基本的には remove() を使えば十分です。
テキストの挿入方法によるつまずきポイント
innerHTMLとtextContentの違い
要素の中身を追加する方法には、createElement を使う以外に、innerHTML に直接文字列を代入する方法もあります。
私が実務で実際にヒヤッとした経験が、この innerHTML の使い方に関するものでした。
❌ Before:ユーザー入力をそのままinnerHTMLに差し込んでしまう
const list = document.querySelector("#comment-list");
const userInput = "<img src=x onerror=alert('乗っ取り成功')>"; // ユーザーが入力した文字列という想定
const li = document.createElement("li");
li.innerHTML = userInput; // ユーザー入力をそのままHTMLとして解釈させてしまう
list.appendChild(li);
// userInputの中にタグが含まれていると、そのままHTMLとして実行されてしまう
innerHTML は、代入した文字列を「HTMLのコード」としてそのまま解釈します。
もしユーザーが入力した文字列の中に <script> タグや <img> タグのようなものが含まれていた場合、意図しないコードが実行されるXSS(クロスサイトスクリプティング)という脆弱性につながる恐れがあります。
私は個人開発のフォームで、この仕組みを知らずに innerHTML を使ってコメント欄を実装してしまい、後からセキュリティのレビューで指摘されて青ざめた経験があります。
✅ After:textContentを使って文字列として安全に挿入する
const list = document.querySelector("#comment-list");
const userInput = "<img src=x onerror=alert('乗っ取り成功')>";
const li = document.createElement("li");
li.textContent = userInput; // 文字列としてそのまま表示され、HTMLとしては解釈されない
list.appendChild(li);
// 画面には「<img src=x onerror=alert('乗っ取り成功')>」という文字列がそのまま表示される
textContent は、代入した文字列をHTMLとしてではなく、ただの文字列として扱います。
ユーザーが入力した内容を画面に表示するときは、innerHTML ではなく textContent を使うのが基本方針です。
innerHTML を使ってよいのは、自分自身が用意した固定のHTML文字列を挿入する場合など、外部からの入力が混ざらないケースに限定しましょう。
複数要素をまとめて追加するときの工夫
DocumentFragmentで再描画のコストを抑える
余談として、リストのアイテムを何十件もまとめて追加するようなケースでは、appendChild をループの中で何度も呼び出すと、そのたびに画面の再描画が発生してパフォーマンスが落ちることがあります。
const list = document.querySelector("#fruit-list");
const fruits = ["いちご", "メロン", "バナナ"];
const fragment = document.createDocumentFragment();
fruits.forEach(function (fruit) {
const li = document.createElement("li");
li.textContent = fruit;
fragment.appendChild(li);
});
list.appendChild(fragment); // まとめて1回だけDOMに追加される
DocumentFragment は「画面には表示されない仮のコンテナ」で、この中に要素を溜めてから最後に一度だけ本物のDOMに追加すると、再描画の回数を1回に抑えられます。
件数が少ないうちは気にしなくても問題ありませんが、こういう考え方があることだけ知っておくと、後々のパフォーマンス改善で役立ちます。
まとめ
この記事のポイント
createElementで新しい要素を作り、appendChildやinsertBeforeでDOMツリーに追加する- 要素の削除は
removeChild(親経由)よりも、remove()(要素自身に呼び出す)の方がシンプル - ユーザー入力を画面に表示するときは、
innerHTMLではなくtextContentを使いXSSを防ぐ - 大量の要素を追加するときは
DocumentFragmentを使うと再描画のコストを抑えられる
次に読むべき記事
要素の追加・削除ができるようになったら、次は見た目に直結する「クラスの操作」を見ていきましょう。
→ 次の記事:classListでクラスを操作する方法