JavaScript:イベント処理
1学習の目的
- addEventListener でクリックや入力に反応する処理を書けるようになる。一度描いたら終わりだった画面が、ユーザーの操作で動くようになる。
- イベントオブジェクトとイベント委譲を理解する。後から追加された要素にも反応する、実用的なアプリの作り方が身につく。
2基礎解説
| やりたいこと | 書き方 | 備考 |
|---|---|---|
| クリックに反応 | el.addEventListener("click", fn) | 最も基本 |
| 入力に反応 | addEventListener("input", fn) | 1文字ごとに発火 |
| 入力値を取る | input.value | 常に文字列 |
| 既定動作を止める | e.preventDefault() | フォーム送信の抑止など |
- addEventListener("イベント名", 関数) が基本形。関数は「登録するだけ」で、呼び出してはいけない。fn() と書くと即実行されてしまう。
- ハンドラはイベントオブジェクトを受け取る。e.target が実際に操作された要素、e.preventDefault() で既定の動作を止められる。
- input.value は常に文字列。数値として使うなら Number() で変換する(JS02・JS10)。
- イベントは親へ伝わっていく(バブリング)。これを利用し、親に1つだけリスナーを付けて e.target で判定するのがイベント委譲。
- 後から追加した要素には、既存のリスナーは付かない。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問)
クリック後 #output が クリックされました
btn.addEventListener("click", () => { ... }) の形。中で output.textContent に代入する。
const btn = document.querySelector("#btn");
const output = document.querySelector("#output");
btn.addEventListener("click", () => {
output.textContent = "クリックされました";
});
「いつ」「何をするか」を登録しておくのがイベント処理。登録した時点では何も起きず、実際に押されたときに初めて中身が動く。
A. 正しく登録される B. 関数がその場で実行され、戻り値が登録される C. エラーになる D. 2回実行される 選択★☆☆無料
解答 == B
カッコを付けると関数はどうなるか。
B
カッコを付けると即実行される。登録したいのは関数そのものなので handleClick とカッコなしで渡す。初学者が最もつまずく点。
入力後 #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文字打つたびに発火する。リアルタイム検索や文字数カウントはこれで作れる。
const btn = document.querySelector("#btn");
btn.____("click", () => {
console.log("押された");
});
クリック後 出力 == 押された
「イベントリスナーを追加する」という意味のメソッド。
addEventListener
同じ要素に何個でも登録できるのがこの方式の利点。onclick = で代入する古い書き方だと、後から書いたもので上書きされてしまう。
A. e.value B. e.target C. e.element D. this.node 選択★☆☆無料
解答 == B
イベントの「対象」を表す英単語。
B
e.target は実際にクリックされた要素、e.currentTarget はリスナーを登録した要素。イベント委譲ではこの違いが決定的に重要になる。
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にリセットされてしまう。この「外の変数を書き換える」形がアプリの基本構造になる。
const form = document.querySelector("#form");
form.addEventListener("submit", (e) => {
e.____();
console.log("送信を止めました");
});
submit後 出力 == 送信を止めました
「既定の動作を防ぐ」という意味のメソッド。
preventDefault
フォームは何もしないとページを再読み込みする。JavaScriptで処理したいときは必ずこれで止める。リンクの遷移を止めるときにも使う。
A. 100個すべてに addEventListener する B. 親要素に1つ登録し e.target で判定する C. HTMLに onclick を書く D. 1秒ごとに状態を確認する 選択★★☆無料
解答 == B
イベントは親へ伝わっていく性質がある。
B
これをイベント委譲と呼ぶ。登録が1つで済むうえ、後から追加されたボタンにも自動で反応する。一覧を動的に作るアプリでは必須の技法。
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 は実際に押された子要素を指すので、これで個別に処理できる。
クリック後 #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問)
※検証では "こんにちは" が入力されるものとする コーディング★★☆無料
入力後 #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行で済む。
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」。これを使うと状態を別の変数で管理する必要がなくなり、ズレる心配もなくなる。
※検証では 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 を忘れるとページが再読み込みされ、表示した内容が一瞬で消える。フォームを扱うときは最初に書く癖をつける。
削除ボタンクリック後 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)も効いている。
クリック後 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仕上げ課題
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つのファイルに全部書いていたコードを、役割ごとに分割できるようになる。