Q1const li = document.createElement("li"); の直後、li はページのどこにありますか?
要素の作成・変更・削除 — createElement / classList / remove
要素を作って加える createElement と append、取り除く remove、class と属性を書き換える classList と dataset、入力を文字のまま表示する textContent を扱います。
一覧に 1 件足す、完了の印を付ける、1 件消すなどの書き換えのたびに innerHTML へ代入すると、残す li まで作り直します。利用者の入力を同じように代入すると、混ざった <a> などのタグまで要素になります。
この記事では、要素を作る・取り除く書き方、class と属性を書き換える classList と dataset、入力を文字のまま入れる書き方を扱います。
要素を作って加える — createElement と append
タスク一覧の末尾に「請求書を送る」を 1 件足したいとします。innerHTML に文字列を足して代入し直すと、並んでいた li もすべて新しい要素に作り直されます。先に querySelector で変数に入れておいた li はページから外れた古い要素を指したままになり、その変数から文字を書き換えても表示は変わりません。
createElement(タグ名を受け取り、DOM ツリーのどこにも入っていない新しい要素を作るメソッド)は document.createElement("li") のように呼びます。作った要素は、append(呼び出した要素の子の末尾に、渡した要素を加えるメソッド)で親の要素に加えると、ページの一部になります。
document.body.innerHTML = `<ul id="tasks"><li>見積書を作る</li><li>議事録を共有する</li></ul>`;
const taskList = document.querySelector("#tasks");
// li 要素を作り、中の文字を入れる(まだページには無い)
const newTask = document.createElement("li");
newTask.textContent = "請求書を送る";
console.log(newTask.outerHTML); // <li>請求書を送る</li>
console.log(document.querySelectorAll("li").length); // 2
// ul の子の末尾に加えると、ページの一部になる
taskList.append(newTask);
console.log(document.querySelectorAll("li").length); // 3
console.log(taskList.outerHTML);
// <ul id="tasks"><li>見積書を作る</li><li>議事録を共有する</li><li>請求書を送る</li></ul>
変数 newTask が指しているのは、append の前も後も同じ li 要素です。そのため append した後に newTask.textContent へ代入すると、ページに並んだ 3 件目の li の文字が書き換わります。
完了の印を付け替える — classList と dataset
完了したタスクに done の class を足し、CSS で取り消し線の見た目にしたいとします。その li の class 属性には task urgent という 1 つの文字列が入っているので、値を done に書き直すと、行の見た目を CSS で決めている task の class まで消えます。
classList(要素の class を 1 語ずつ扱うオブジェクト)の add は語を足し、remove は外し、toggle は無ければ足してあれば外します。class 以外の属性は setAttribute(名前, 値) で書き、data- で始まる属性は dataset(data 属性を名前ごとに読み書きするオブジェクト)で書きます。
document.body.innerHTML = `<ul><li class="task urgent" data-id="T-1">見積書を作る</li><li class="task urgent" data-id="T-2">請求書を送る</li></ul>`;
const quote = document.querySelector('[data-id="T-1"]');
const invoice = document.querySelector('[data-id="T-2"]');
// classList は 1 語ずつ足す・外す(ほかの class は残る)
quote.classList.add("done");
console.log(quote.outerHTML); // <li class="task urgent done" data-id="T-1">見積書を作る</li>
quote.classList.toggle("urgent"); // 完了したので急ぎの class を外す(付いているので外れる)
console.log(quote.outerHTML); // <li class="task done" data-id="T-1">見積書を作る</li>
// setAttribute は値をまるごと置き換える(class に使うと task も消える)
invoice.setAttribute("class", "done");
console.log(invoice.outerHTML); // <li class="done" data-id="T-2">請求書を送る</li>
// data- で始まる属性は dataset で書く(doneAt は data-done-at になる)
invoice.dataset.doneAt = "9/10";
console.log(invoice.outerHTML);
// <li class="done" data-id="T-2" data-done-at="9/10">請求書を送る</li>
dataset で使う名前は、属性の名前から data- を外し、残りのハイフンの次の文字を大文字にしたものです。data-id は dataset.id、data-done-at は dataset.doneAt で読み書きし、代入すると元の属性の名前で HTML に書き込まれます。
dataset にハイフンの名前は使えない
dataset["done-at"] のようにハイフンのまま読むと、エラーにならずに undefined が返るので、書き間違いに気づきにくくなります。代入するとその行で止まり、エラー名は SyntaxError ですが、文法の誤りとは違って前の行までは実行されます。
要素を取り除く — remove と変数に残る参照
完了したタスクを、一覧から取り除きたいとします。innerHTML で書き直すと、前の章で足した class や data 属性も HTML の文字列に書き込み直す必要があり、1 つでも書き漏らすと、残したタスクから完了の印が消えます。
remove(呼び出した要素を親から外し、DOM ツリーから取り除くメソッド)は、消したい要素を探して doneItem.remove() のように引数なしで呼びます。前の章の classList.remove("done") が外すのは class の語だけで、要素そのものを外すこちらとは別のメソッドです。
document.body.innerHTML = `<ul id="tasks"><li data-id="T-1">見積書を作る</li><li data-id="T-2" class="done">請求書を送る</li><li data-id="T-3">議事録を共有する</li></ul>`;
const tasks = document.querySelectorAll("li");
// 完了した li を探して、DOM ツリーから取り除く
const doneItem = document.querySelector(".done");
doneItem.remove();
console.log(document.querySelectorAll("li").length); // 2
// 取り除いた要素も、先に取得した NodeList も、変数には残る
console.log(doneItem.textContent); // 請求書を送る
console.log(tasks.length); // 3(取得した時点の 3 件)
// 変数に残っているので、append で一覧の末尾へ戻せる
document.querySelector("#tasks").append(doneItem);
console.log(document.querySelectorAll("li").length); // 3
querySelectorAll は呼んだ時点で見つかった要素を並べて返すだけで、その後の DOM の変化を追いかけません。取り除いた doneItem も変数に残るので、文字を読むことも、append で一覧へ戻すこともできます。
入力を文字として出す — textContent と innerHTML
商品レビューの欄に、利用者が投稿した感想を表示したいとします。感想は保存され、ほかの閲覧者のページにも同じ文字列で表示されます。感想に a タグ(リンクを作るタグ。href 属性に移動先を書く)が含まれていると、innerHTML への代入で、投稿者が用意した偽のリンクが本物のリンクとして並びます。
innerHTML に代入した文字列は HTML として解析され、タグは要素になります。textContent に代入した文字列は解析されず、< もただの文字として入ります。入力を innerHTML に入れると、偽のリンクを並べられるだけでなく、XSS(利用者が入力に混ぜたコードが、ほかの閲覧者のページで実行される脆弱性)の原因にもなります。
document.body.innerHTML = `<p class="review"></p>`;
const review = document.querySelector(".review");
// 利用者が感想の欄に入力した文字列(偽のリンクが混ざっている)
const reviewText = '<a href="https://example.com/login">再ログイン</a>してください';
// innerHTML に入れると、a タグが本物の a 要素になる
review.innerHTML = reviewText;
console.log(document.querySelectorAll("a").length); // 1
// textContent に入れると、タグも文字のまま入る
review.textContent = reviewText;
console.log(document.querySelectorAll("a").length); // 0
console.log(review.outerHTML);
// <p class="review"><a href="https://example.com/login">再ログイン</a>してください</p>
- 中の文字は「再ログイン」
- 押すと別のサイトへ移動する
- a 要素の後ろに続く
<a href="…">再ログイン</a>してください- a 要素は 0 件
outerHTML に出る < と > は、HTML の中で < と > を文字として書く形で、textContent で読み出すと入力どおりの文字に戻ります。下の表は、この記事で扱った書き換えの方法を、書き換わる場所ごとにまとめたものです。
| 書き換える場所 | 書き方 | 気をつけること |
|---|---|---|
| 要素そのもの | createElement / append / remove | append するまでページに無い |
| class | classList.add / remove / toggle | setAttribute で書くと値がまるごと置き換わる |
| data- で始まる属性 | dataset.doneAt = "9/10" | data- を外し、ハイフンの次を大文字にした名前で書く |
| そのほかの属性 | setAttribute("href", "/tasks/T-2") | 無ければ足し、あれば値を置き換える |
| 中身の文字 | textContent | 利用者の入力はこちらに入れる |
| 中身の HTML | innerHTML | 自分で書いた HTML だけを入れる |
img の onerror に書いたコードは動く
innerHTML に入れた <script> は実行されませんが、画像を表示する img タグを <img src="x" onerror="…"> と書くと、onerror(読み込みに失敗したときに動くコードを書く属性)が動きます。script が動かないことは、innerHTML に入れてよい理由になりません。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2class が task urgent の li で setAttribute("class", "done") を呼ぶと、class は?
Q3p.textContent = "<b>新着</b>"; の後、p の中に b 要素はいくつありますか?