順番に読み進めながら学べます

要素の作成・変更・削除 — createElement / classList / remove

要素を作って加える createElement と append、取り除く remove、class と属性を書き換える classList と dataset、入力を文字のまま表示する textContent を扱います。

一覧に 1 件足す、完了の印を付ける、1 件消すなどの書き換えのたびに innerHTML へ代入すると、残す li まで作り直します。利用者の入力を同じように代入すると、混ざった <a> などのタグまで要素になります。

この記事では、要素を作る・取り除く書き方、class と属性を書き換える classListdataset、入力を文字のまま入れる書き方を扱います。

要素を作って加える — 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>
作った li がページに入るまで
createElement("li")newTask.textContent= "請求書を送る"taskList.append(newTask)<li></li> ができるページの li は 2 件<li>請求書を送る</li>まだページの外ul の末尾に入りli が 3 件になる
createElement と textContent の行を終えても、li は 2 件のままです。件数が増えるのは、append を呼んだ行です

変数 newTask が指しているのは、append の前も後も同じ li 要素です。そのため append した後に newTask.textContent へ代入すると、ページに並んだ 3 件目の li の文字が書き換わります。

対応待ち一覧に、新しく届いた問い合わせをまとめて加えます。一覧の ul 要素 inquiryList と、問い合わせの件名を並べた配列 newInquiries は宣言済みです。

① 配列の件名を 1 件ずつ li 要素にし、一覧の末尾に加えてください。

② ページにある li の件数を表示してください。

③ ul 要素のタグも含めて、一覧の HTML を表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

完了の印を付け替える — 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>
add は足し、setAttribute は置き換える
quote.classList.add("done")task urgent にdone を 1 語足すclass はtask urgent donetask の行の見た目は残るinvoice.setAttribute("class", "done")class の値をまるごと置き換えclass はdone だけtask の行の見た目が消える
add は 2 語を残して足し、setAttribute は値を done に置き換えます。class を足すときは classList を使います

dataset で使う名前は、属性の名前から data- を外し、残りのハイフンの次の文字を大文字にしたものです。data-iddataset.iddata-done-atdataset.doneAt で読み書きし、代入すると元の属性の名前で HTML に書き込まれます。

dataset にハイフンの名前は使えない

dataset["done-at"] のようにハイフンのまま読むと、エラーにならずに undefined が返るので、書き間違いに気づきにくくなります。代入するとその行で止まり、エラー名は SyntaxError ですが、文法の誤りとは違って前の行までは実行されます。

入社手続きのチェックリストで、完了の class を付け替えます。リストの HTML は宣言済みです。

① 番号が C-2 の項目に、done の class を足してください。

② 誤って完了にしていた C-1 の項目の done を、1 回の呼び出しで切り替えてください。

③ C-2 の項目に、完了日の data 属性を書いてください。

④ ul 要素のタグも含めて、リストの HTML を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

要素を取り除く — 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("li") を呼び直すremove の後のDOM を探すT-1 と T-3の lilength は 2tasksremove の前に取得取得した時点の並びのままT-1・T-2・T-3の lilength は 3
remove の後に呼び直すと T-1 と T-3 だけが見つかり、tasks は 3 件のままです。件数を確かめるときは、呼び直します

querySelectorAll は呼んだ時点で見つかった要素を並べて返すだけで、その後の DOM の変化を追いかけません。取り除いた doneItem も変数に残るので、文字を読むことも、append で一覧へ戻すこともできます。

メールの作成画面で、添付を取り消したファイルを一覧からまとめて取り除きます。一覧の HTML と、一覧の ul 要素 attachmentList は宣言済みです。

① 取り消したファイルの要素をすべて取得し、1 件ずつ一覧から取り除いてください。

② 取り除いた後に、① で取得したものの件数を表示してください。

③ 取り消したファイルの要素を取得し直し、件数を表示してください。

④ ul 要素のタグも含めて、一覧の HTML を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

入力を文字として出す — 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">&lt;a href="https://example.com/login"&gt;再ログイン&lt;/a&gt;してください</p>
同じ入力を入れた後の p の中身
innerHTML に代入した p
a 要素 — href は example.com/login
  • 中の文字は「再ログイン」
  • 押すと別のサイトへ移動する
文字 — してください
  • a 要素の後ろに続く
textContent に代入した p
文字 — 入力の全体で 1 つ
  • <a href="…">再ログイン</a>してください
  • a 要素は 0 件
innerHTML では a タグが要素になり、textContent では文字のまま入ります。入力を要素にしないのは textContent です

outerHTML に出る &lt;&gt; は、HTML の中で <> を文字として書く形で、textContent で読み出すと入力どおりの文字に戻ります。下の表は、この記事で扱った書き換えの方法を、書き換わる場所ごとにまとめたものです。

書き換える場所書き方気をつけること
要素そのものcreateElement / append / removeappend するまでページに無い
classclassList.add / remove / togglesetAttribute で書くと値がまるごと置き換わる
data- で始まる属性dataset.doneAt = "9/10"data- を外し、ハイフンの次を大文字にした名前で書く
そのほかの属性setAttribute("href", "/tasks/T-2")無ければ足し、あれば値を置き換える
中身の文字textContent利用者の入力はこちらに入れる
中身の HTMLinnerHTML自分で書いた HTML だけを入れる

img の onerror に書いたコードは動く

innerHTML に入れた <script> は実行されませんが、画像を表示する img タグを <img src="x" onerror="…"> と書くと、onerror(読み込みに失敗したときに動くコードを書く属性)が動きます。script が動かないことは、innerHTML に入れてよい理由になりません。

商品レビューの欄に、投稿を 1 件表示します。投稿者名 author・感想 comment・欄の要素 reviewArea は宣言済みです。

① article 要素を作り、投稿者名を入れた strong 要素と、感想を入れた p 要素を順に中へ加えてください。入力は文字のまま入れてください。

② article を欄に加え、ページの a 要素の件数を表示してください。

③ article 要素のタグも含めて HTML を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1const li = document.createElement("li"); の直後、li はページのどこにありますか?

Q2class が task urgent の li で setAttribute("class", "done") を呼ぶと、class は?

Q3p.textContent = "<b>新着</b>"; の後、p の中に b 要素はいくつありますか?