JavaScript / LESSON 12 / JS12

JavaScript:イベント処理

1学習の目的

2基礎解説

やりたいこと書き方備考
クリックに反応el.addEventListener("click", fn)最も基本
入力に反応addEventListener("input", fn)1文字ごとに発火
入力値を取るinput.value常に文字列
既定動作を止めるe.preventDefault()フォーム送信の抑止など
✅ 覚えるべき重要ポイント
  1. addEventListener("イベント名", 関数) が基本形。関数は「登録するだけ」で、呼び出してはいけないfn() と書くと即実行されてしまう。
  2. ハンドラはイベントオブジェクトを受け取る。e.target が実際に操作された要素、e.preventDefault() で既定の動作を止められる。
  3. input.value常に文字列。数値として使うなら Number() で変換する(JS02・JS10)。
  4. イベントは親へ伝わっていく(バブリング)。これを利用し、親に1つだけリスナーを付けて e.target で判定するのがイベント委譲
  5. 後から追加した要素には、既存のリスナーは付かない。100個のボタンに個別登録するより、親に1つ付けるほうが速く、追加分にも自動で対応できる。

現場使用例:ボタンで表示切り替え、入力欄の文字数カウント、フォーム送信前の検証、一覧の削除ボタン、リアルタイム検索。

const button = document.querySelector("#btn");
const output = document.querySelector("#output");

// クリックに反応する(関数は呼び出さずに渡す)
button.addEventListener("click", () => {
  output.textContent = "クリックされました";
});

// 入力に反応する
const input = document.querySelector("#name");

input.addEventListener("input", (e) => {
  output.textContent = `${e.target.value.length}文字`;
});

// イベント委譲:親に1つ付けて子を判定する
const list = document.querySelector("#list");

list.addEventListener("click", (e) => {
  if (e.target.classList.contains("delete")) {
    e.target.parentNode.remove();
  }
});

3基本ドリル(10問)

JS12-D01 id が "btn" のボタンにクリックイベントを登録し、押されたら id が "output" の要素に「クリックされました」と表示せよ。 コーディング★☆☆無料
期待される結果

クリック後 #output が クリックされました

ヒント

btn.addEventListener("click", () => { ... }) の形。中で output.textContent に代入する。

模範解答
const btn = document.querySelector("#btn");
const output = document.querySelector("#output");

btn.addEventListener("click", () => {
  output.textContent = "クリックされました";
});
解説

「いつ」「何をするか」を登録しておくのがイベント処理。登録した時点では何も起きず、実際に押されたときに初めて中身が動く。

JS12-D02 btn.addEventListener("click", handleClick()); と書くとどうなるか。
A. 正しく登録される B. 関数がその場で実行され、戻り値が登録される C. エラーになる D. 2回実行される
選択★☆☆無料
期待される結果

解答 == B

ヒント

カッコを付けると関数はどうなるか。

模範解答
B
解説

カッコを付けると即実行される。登録したいのは関数そのものなので handleClick とカッコなしで渡す。初学者が最もつまずく点。

JS12-D03 id が "name" の入力欄に input イベントを登録し、入力された文字を id が "output" に表示せよ(e.target.value を使うこと)。 コーディング★☆☆無料
期待される結果

入力後 #output に入力値が反映

ヒント

ハンドラは (e) => { ... } の形で受け取り、e.target.value で入力値が取れる。

模範解答
const input = document.querySelector("#name");
const output = document.querySelector("#output");

input.addEventListener("input", (e) => {
  output.textContent = e.target.value;
});
解説

input イベントは1文字打つたびに発火する。リアルタイム検索や文字数カウントはこれで作れる。

JS12-D04 クリックイベントを登録するコードを完成させよ。 穴埋め★☆☆無料
コード
const btn = document.querySelector("#btn");

btn.____("click", () => {
  console.log("押された");
});
期待される結果

クリック後 出力 == 押された

ヒント

「イベントリスナーを追加する」という意味のメソッド。

模範解答
addEventListener
解説

同じ要素に何個でも登録できるのがこの方式の利点。onclick = で代入する古い書き方だと、後から書いたもので上書きされてしまう。

JS12-D05 イベントハンドラの中で、実際に操作された要素を指すのはどれか。
A. e.value B. e.target C. e.element D. this.node
選択★☆☆無料
期待される結果

解答 == B

ヒント

イベントの「対象」を表す英単語。

模範解答
B
解説

e.target は実際にクリックされた要素、e.currentTarget はリスナーを登録した要素。イベント委譲ではこの違いが決定的に重要になる。

JS12-D06 id が "btn" のボタンを押すたびに、id が "count" の表示を 1 ずつ増やせ(初期値は 0、3回押した結果を確認する)。 コーディング★★☆無料
期待される結果

3回クリック後 #count が 3

ヒント

let count = 0 をリスナーの外に用意し、ハンドラの中で count++ して表示を更新する。

模範解答
const btn = document.querySelector("#btn");
const display = document.querySelector("#count");

let count = 0;

btn.addEventListener("click", () => {
  count++;
  display.textContent = count;
});
解説

状態を持つ変数はリスナーの外に置く。中で宣言すると毎回0にリセットされてしまう。この「外の変数を書き換える」形がアプリの基本構造になる。

JS12-D07 フォーム送信時のページ再読み込みを止めるコードを完成させよ。 穴埋め★★☆無料
コード
const form = document.querySelector("#form");

form.addEventListener("submit", (e) => {
  e.____();
  console.log("送信を止めました");
});
期待される結果

submit後 出力 == 送信を止めました

ヒント

「既定の動作を防ぐ」という意味のメソッド。

模範解答
preventDefault
解説

フォームは何もしないとページを再読み込みする。JavaScriptで処理したいときは必ずこれで止める。リンクの遷移を止めるときにも使う。

JS12-D08 ボタンを100個並べる。クリック処理を効率よく実装する方法はどれか。
A. 100個すべてに addEventListener する B. 親要素に1つ登録し e.target で判定する C. HTMLに onclick を書く D. 1秒ごとに状態を確認する
選択★★☆無料
期待される結果

解答 == B

ヒント

イベントは親へ伝わっていく性質がある。

模範解答
B
解説

これをイベント委譲と呼ぶ。登録が1つで済むうえ、後から追加されたボタンにも自動で反応する。一覧を動的に作るアプリでは必須の技法。

JS12-D09 id が "list" の要素に click を1つだけ登録し、クリックされた要素が "item" クラスを持つ場合だけ、その要素に "selected" クラスを付けよ(イベント委譲)。 コーディング★★☆無料
期待される結果

li クリック後 その要素に selected が付く

ヒント

e.target.classList.contains("item") で判定してから add する。

模範解答
const list = document.querySelector("#list");

list.addEventListener("click", (e) => {
  if (e.target.classList.contains("item")) {
    e.target.classList.add("selected");
  }
});
解説

親で受けて子を判定するのがイベント委譲の形。e.target は実際に押された子要素を指すので、これで個別に処理できる。

JS12-D10 id が "num" の入力欄(値は文字列 "5")から値を取り、2倍にして id が "output" に表示せよ。ボタン "#btn" のクリックで実行すること。 コーディング★★☆無料
期待される結果

クリック後 #output が 10

ヒント

input.value は文字列なので Number() で変換する。変換しないと "55" になる。

模範解答
const btn = document.querySelector("#btn");
const num = document.querySelector("#num");
const output = document.querySelector("#output");

btn.addEventListener("click", () => {
  output.textContent = Number(num.value) * 2;
});
解説

input.value は必ず文字列。JS02で学んだとおり、変換を忘れると "5" * 2 は10になるが "5" + 2 は "52" になる。計算前の Number() は習慣にする。

4実践シナリオ(5問)

JS12-S01 【文字数カウンター】id が "text" の入力欄に文字が入力されるたび、id が "counter" に「12 / 100文字」の形式で表示せよ。100文字を超えたら counter に "over" クラスを付け、そうでなければ外すこと。
※検証では "こんにちは" が入力されるものとする
コーディング★★☆無料
期待される結果

入力後 #counter が 5 / 100文字

ヒント

e.target.value.length で文字数が取れる。classList.toggle("over", 条件) と書くと条件に応じて付け外しできる。

模範解答
const text = document.querySelector("#text");
const counter = document.querySelector("#counter");
const LIMIT = 100;

text.addEventListener("input", (e) => {
  const length = e.target.value.length;
  counter.textContent = `${length} / ${LIMIT}文字`;
  counter.classList.toggle("over", length > LIMIT);
});
解説

classList.toggle(名前, 条件) は第2引数で付け外しを制御できる。if で add / remove を書き分けるより1行で済む。

JS12-S02 【表示の切り替え】id が "toggleBtn" を押すたびに、id が "panel" の "hidden" クラスを付け外しし、ボタンの文字を「開く」「閉じる」で切り替えよ。初期状態は hidden あり(=閉じている)で、ボタンは「開く」。 コーディング★★☆無料
期待される結果

1回目で hidden が外れ文字が 閉じる、2回目で戻る

ヒント

classList.toggle は付け外しした結果を true / false で返す。その戻り値でボタンの文字を決められる。

模範解答
const toggleBtn = document.querySelector("#toggleBtn");
const panel = document.querySelector("#panel");

toggleBtn.addEventListener("click", () => {
  const isHidden = panel.classList.toggle("hidden");
  toggleBtn.textContent = isHidden ? "開く" : "閉じる";
});
解説

toggle の戻り値は「付けたら true、外したら false」。これを使うと状態を別の変数で管理する必要がなくなり、ズレる心配もなくなる。

JS12-S03 【入力の検証】id が "form" の submit を止めたうえで、id が "email" の値に @ が含まれるか検証せよ。含まれなければ id が "msg" に「メール形式が不正です」と表示し "error" クラスを付ける。正しければ「送信しました」と表示し "error" を外すこと。
※検証では email に "test" が入っているものとする
コーディング★★☆無料
期待される結果

submit後 #msg が メール形式が不正です で error クラスあり

ヒント

e.preventDefault() を最初に書く。判定は value.includes("@")。

模範解答
const form = document.querySelector("#form");
const email = document.querySelector("#email");
const msg = document.querySelector("#msg");

form.addEventListener("submit", (e) => {
  e.preventDefault();

  const isValid = email.value.includes("@");

  msg.textContent = isValid ? "送信しました" : "メール形式が不正です";
  msg.classList.toggle("error", !isValid);
});
解説

preventDefault を忘れるとページが再読み込みされ、表示した内容が一瞬で消える。フォームを扱うときは最初に書く癖をつける。

JS12-S04 【削除ボタン付き一覧】id が "list" に <li>項目<button class="del">削除</button></li> の形の項目が3つある。イベント委譲で、削除ボタンが押されたらその li を削除し、「残り2件」と出力せよ。 コーディング★★★無料
期待される結果

削除ボタンクリック後 li が2つ、出力 == 残り2件

ヒント

list に1つだけリスナーを登録し、e.target が del クラスなら e.target.parentNode.remove() する。件数は list.children.length。

模範解答
const list = document.querySelector("#list");

list.addEventListener("click", (e) => {
  if (!e.target.classList.contains("del")) {
    return;
  }

  e.target.parentNode.remove();
  console.log(`残り${list.children.length}件`);
});
解説

個々の削除ボタンにリスナーを付けていないのが要点。だから後から項目を追加しても、そのボタンは何もしなくても動く。冒頭の return で早期に抜ける書き方(JS08)も効いている。

JS12-S05 【項目の追加】id が "input"(値は "新しいタスク")と id が "addBtn" がある。ボタンを押したら input の値で li を作って id が "list" に追加し、入力欄を空にせよ。空文字のときは追加しないこと。追加後に「1件追加(合計1件)」と出力すること。 コーディング★★★無料
期待される結果

クリック後 li が1つ追加され input が空、出力 == 1件追加(合計1件)

ヒント

trim() で前後の空白を除いてから空判定する。追加後に input.value = "" で空にする。

模範解答
const input = document.querySelector("#input");
const addBtn = document.querySelector("#addBtn");
const list = document.querySelector("#list");

addBtn.addEventListener("click", () => {
  const text = input.value.trim();

  if (!text) {
    return;
  }

  const li = document.createElement("li");
  li.textContent = text;
  list.appendChild(li);

  input.value = "";

  console.log(`1件追加(合計${list.children.length}件)`);
});
解説

入力 → 検証 → 追加 → 入力欄をクリアという流れは、あらゆる入力フォームの基本形。trim() を挟まないと空白だけの項目が追加されてしまう。

5仕上げ課題

JS12-FINAL 【ToDoリストを作る】追加・完了・削除ができるToDoリストを実装せよ。

HTMLには次の要素があるものとする:
#input(入力欄)/#addBtn(追加ボタン)/#list(ul)/#summary(p)

仕様:
追加:ボタンを押すと入力値で li を作り list に追加。li の中身は「テキスト+削除ボタン(class="del"、文字は「削除」)」。空文字なら何もしない。追加後に入力欄をクリア
完了:li のテキスト部分をクリックすると "done" クラスを付け外しする(イベント委譲)
削除:削除ボタンを押すとその li を削除(イベント委譲)
集計:追加・完了・削除のたび #summary に「全2件 / 完了1件」の形式で表示

検証の流れ:「買い物」を追加 → 「掃除」を追加 → 1件目を完了 → 2件目を削除
期待される最終状態:li が1つ、それに done クラスが付き、summary が「全1件 / 完了1件」
期待される結果

最終状態 li 1つ・done あり・summary が 全1件 / 完了1件

ヒント

集計を updateSummary() という関数に切り出し、3つの操作すべてから呼ぶ。完了と削除は list に1つのリスナーを登録し、e.target が del クラスかどうかで分岐する。テキスト部分のクリックは e.target が li 自身のときと考えてよい。

模範解答
const input = document.querySelector("#input");
const addBtn = document.querySelector("#addBtn");
const list = document.querySelector("#list");
const summary = document.querySelector("#summary");

const updateSummary = () => {
  const all = list.children.length;
  const done = list.querySelectorAll(".done").length;
  summary.textContent = `全${all}件 / 完了${done}件`;
};

addBtn.addEventListener("click", () => {
  const text = input.value.trim();

  if (!text) {
    return;
  }

  const li = document.createElement("li");
  li.textContent = text;

  const del = document.createElement("button");
  del.textContent = "削除";
  del.classList.add("del");
  li.appendChild(del);

  list.appendChild(li);
  input.value = "";
  updateSummary();
});

list.addEventListener("click", (e) => {
  if (e.target.classList.contains("del")) {
    e.target.parentNode.remove();
    updateSummary();
    return;
  }

  if (e.target.tagName === "LI") {
    e.target.classList.toggle("done");
    updateSummary();
  }
});
解説

ついに「動くWebアプリ」ができた。追加・完了・削除という3つの操作すべてに反応し、状態が変わるたびに集計が更新される——これは実際のサービスと同じ構造をしている。

設計上の要点は3つ。第一に、集計を updateSummary() に切り出したこと。3箇所から呼ばれるが、表示形式を変えたければこの関数だけ直せばよい(JS08)。第二に、完了と削除を1つのリスナーで処理していること。li は後から追加されるので、個別登録では新しい項目が動かない。第三に、e.target で分岐していること——押されたのがボタンなのかli本体なのかを見分けて処理を分けている。

ただしこのアプリには重大な弱点がある。ページを再読み込みするとすべて消える。データがメモリ上にしか無いからだ。JS17のローカルストレージを学ぶと、この問題が解決する。

次章ではモジュールを学ぶ。1つのファイルに全部書いていたコードを、役割ごとに分割できるようになる。