Chapter 04

イベント処理

addEventListener、イベントオブジェクト、イベント委任、フォームイベントなど、インタラクティブなUIの基礎を学びます。

30 min

このチャプターで学ぶこと

  • addEventListener でイベントリスナーを登録できる
  • イベントオブジェクトから必要な情報を取得できる
  • イベント委任(Event Delegation)のパターンを使える
  • フォームの submit イベントを処理できる
  • removeEventListener でリスナーを解除できる

イベント処理

前のチャプターでHTML要素を取得・操作する方法を学びました。でも「ボタンをクリックしたとき」「テキストを入力したとき」のように、ユーザーの操作に反応するにはどうすればいいでしょうか。それがこのチャプターのテーマ、イベント処理です。

addEventListener の基本

addEventListener は、HTML要素に**イベントリスナー(イベントが起きたときに実行する関数)**を登録するメソッドです。

// 書き方
element.addEventListener('イベント名', コールバック関数);

最もシンプルな例から見てみましょう。

<!-- HTML -->
<button id="my-btn">クリックしてね</button>
<p id="message"></p>
// JavaScript
const btn = document.querySelector('#my-btn');
const message = document.querySelector('#message');

// click イベントが発生したとき(ボタンがクリックされたとき)に実行する
btn.addEventListener('click', () => {
  message.textContent = 'ボタンがクリックされました!';
});

コールバック関数はアロー関数 () => {} で書くのが現代のスタイルです。

古い書き方との比較

昔はHTML属性に直接JavaScriptを書く方法がよく使われていましたが、現代では避けるべきとされています。

<!-- 古いやり方(非推奨) -->
<button onclick="handleClick()">クリック</button>

<!-- 現代のやり方:HTMLとJavaScriptを分離する -->
<button id="my-btn">クリック</button>
// JavaScriptファイルで addEventListener を使う(推奨)
document.querySelector('#my-btn').addEventListener('click', handleClick);

function handleClick() {
  console.log('クリックされました');
}

HTMLとJavaScriptを分けることで、コードの見通しが良くなり、同じ要素に複数のリスナーも登録できます。

📝 jQueryやLivewireとの比較

jQueryを使ったことがある方は $('#btn').on('click', fn) という書き方に慣れているかもしれません。addEventListener はその標準版です。

LaravelのLivewireでは wire:click="methodName" のようにHTMLに直接書きますが、あれはLivewireがフレームワーク側でうまく管理しているためです。素のJavaScriptでは addEventListener を使いましょう。

イベントオブジェクト

コールバック関数にはイベントオブジェクトが自動的に渡されます。慣習的に e または event という変数名で受け取ります。

btn.addEventListener('click', (e) => {
  console.log(e);           // イベントオブジェクト全体
  console.log(e.type);      // "click"(イベントの種類)
  console.log(e.target);    // クリックされた要素そのもの
  console.log(e.timeStamp); // イベントが発生した時刻
});

event.target と event.currentTarget

// ボタンの中に <span> がある場合
// <button id="btn"><span>テキスト</span></button>

const btn = document.querySelector('#btn');

btn.addEventListener('click', (e) => {
  // e.target: 実際にクリックされた要素(spanかもしれない)
  console.log(e.target);

  // e.currentTarget: addEventListener を登録した要素(常にbtn)
  console.log(e.currentTarget);
});

e.target はクリックした「正確な場所」、e.currentTarget はリスナーを登録した「要素」です。後述するイベント委任で重要になります。

event.preventDefault()

リンクのナビゲートやフォームの送信など、ブラウザのデフォルト動作をキャンセルするときに使います。

// リンクのデフォルト動作(ページ遷移)をキャンセル
const link = document.querySelector('a#my-link');
link.addEventListener('click', (e) => {
  e.preventDefault(); // ページ遷移しない
  console.log('リンクがクリックされましたが、ページは移動しません');
});

// フォームのデフォルト動作(ページリロードして送信)をキャンセル
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
  e.preventDefault(); // リロードしない
  // フォームの値をJavaScriptで処理する
});

event.stopPropagation()

イベントが親要素に伝わるバブリングを止めるときに使います(詳しくは後述)。

const inner = document.querySelector('#inner');
inner.addEventListener('click', (e) => {
  e.stopPropagation(); // 親要素へのバブリングを止める
  console.log('内側だけ反応する');
});

よく使うイベント一覧

クリック・マウス

// クリック
btn.addEventListener('click', () => { /* ... */ });

// ダブルクリック
btn.addEventListener('dblclick', () => { /* ... */ });

// マウスが要素に乗ったとき
el.addEventListener('mouseenter', () => { /* ... */ });

// マウスが要素から離れたとき
el.addEventListener('mouseleave', () => { /* ... */ });

テキスト入力

const input = document.querySelector('#search');

// 入力するたびにリアルタイムで発火
input.addEventListener('input', (e) => {
  console.log(e.target.value); // 現在の入力値
});

// フォーカスが外れたときに発火(入力完了後)
input.addEventListener('change', (e) => {
  console.log(e.target.value);
});

// フォーカスを得たとき
input.addEventListener('focus', () => { /* ... */ });

// フォーカスを失ったとき
input.addEventListener('blur', () => { /* ... */ });
📝 input と change の違い

input イベントはキーを押すたびに即座に発火します。検索フィールドのリアルタイムフィルタリングに向いています。

change イベントはフィールドのフォーカスが外れたときに発火します。最終的な値だけを扱いたいときに使います。

テキスト入力のリアルタイム処理には基本的に input を使いましょう。

キーボード

document.addEventListener('keydown', (e) => {
  console.log(e.key);    // "Enter", "a", "ArrowLeft" など
  console.log(e.code);   // "KeyA", "Enter" など

  // Enterキーが押されたとき
  if (e.key === 'Enter') {
    console.log('Enterが押されました');
  }

  // Ctrl + S
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault(); // ブラウザの保存ダイアログをキャンセル
    console.log('保存ショートカット');
  }
});

// keyup はキーを離したときに発火
document.addEventListener('keyup', (e) => { /* ... */ });

ページ読み込み

// HTMLのパースが完了したときに発火(scriptの配置に関わらず安全)
document.addEventListener('DOMContentLoaded', () => {
  // ここで querySelector を使えば要素が必ず存在する
  const app = document.querySelector('#app');
});
💡 DOMContentLoaded はほぼ必須

<script> タグをHTMLの <head> に書くと、HTML要素がまだ読み込まれていない状態でJavaScriptが実行されてしまい、querySelectornull を返すことがあります。DOMContentLoaded イベントを使うか、<script> タグを </body> の直前に書くことで、DOM要素が確実に存在する状態でJavaScriptを実行できます。

イベントバブリング

イベントは**子要素から親要素へと伝播(バブリング)**します。内側の要素でクリックが起きると、その親、さらにその親… と順番にイベントが伝わります。

<div id="outer">
  <div id="middle">
    <button id="inner">クリック</button>
  </div>
</div>
document.querySelector('#outer').addEventListener('click', () => {
  console.log('outer がクリックされた');
});
document.querySelector('#middle').addEventListener('click', () => {
  console.log('middle がクリックされた');
});
document.querySelector('#inner').addEventListener('click', () => {
  console.log('inner がクリックされた');
});

// ボタンをクリックすると以下の順で出力される:
// "inner がクリックされた"
// "middle がクリックされた"
// "outer がクリックされた"

バブリングを止めたい場合は e.stopPropagation() を使います。しかし多くの場合、バブリングはむしろ活用できる仕組みです。それが次に説明するイベント委任です。

💡 バブリングは活用できる

バブリングは一見すると「余計なイベントが発火する」と感じるかもしれませんが、うまく使うとコードを大幅に簡略化できます。特にリストや動的に追加される要素を扱うときに力を発揮します。

イベント委任(Event Delegation)

イベント委任とは、各子要素にリスナーを登録する代わりに、親要素ひとつにリスナーを登録して、event.target で実際にクリックされた子要素を判別するパターンです。

委任しない場合(非効率)

<ul id="menu">
  <li>ホーム</li>
  <li>About</li>
  <li>お問い合わせ</li>
</ul>
// 各liにリスナーを登録(非効率)
const items = document.querySelectorAll('#menu li');
items.forEach((item) => {
  item.addEventListener('click', (e) => {
    console.log(e.target.textContent);
  });
});

委任する場合(効率的)

// 親のulひとつにリスナーを登録(効率的)
const menu = document.querySelector('#menu');

menu.addEventListener('click', (e) => {
  // クリックされたのが li かどうかを確認
  if (e.target.tagName === 'LI') {
    console.log(e.target.textContent);
  }
});

イベント委任が特に重要な場面

動的に追加された要素には、事前に登録したリスナーが効きません。しかしイベント委任なら、後から追加された要素にも自動的に対応できます。

const list = document.querySelector('#todo-list');

// 親にリスナーを登録しておく
list.addEventListener('click', (e) => {
  // data-action 属性で何をするか判別
  const action = e.target.dataset.action;

  if (action === 'delete') {
    // クリックされたボタンの親のli要素を削除
    e.target.closest('li').remove();
  }
});

// 後から追加したアイテムにも自動でリスナーが効く
function addItem(text) {
  const li = document.createElement('li');
  li.innerHTML = `
    <span>${text}</span>
    <button data-action="delete">削除</button>
  `;
  list.appendChild(li);
}

addItem('牛乳を買う');
addItem('洗濯をする');
📝 Reactはイベント委任を自動でやってくれる

後でReactを学ぶと、この仕組みが内部的に自動化されているのがわかります。Reactは仮想DOMを使い、document レベルにイベントをまとめて登録するため、開発者がイベント委任を意識する必要がありません。素のJavaScriptでこのパターンを理解しておくと、Reactの動作原理の理解にもつながります。

フォームイベント

フォームを扱う場面は非常に多いです。基本的なパターンをしっかり押さえましょう。

フォームの送信処理

<form id="search-form">
  <input type="text" id="search-input" placeholder="検索...">
  <button type="submit">検索</button>
</form>
<ul id="results"></ul>
// サンプルデータ
const articles = [
  'JavaScriptの基本',
  'DOM操作入門',
  'イベント処理の基礎',
  'Reactで始めるフロントエンド',
  'Laravelと連携するAPI開発',
];

const form = document.querySelector('#search-form');
const input = document.querySelector('#search-input');
const results = document.querySelector('#results');

form.addEventListener('submit', (e) => {
  // デフォルトの送信(ページリロード)をキャンセル
  e.preventDefault();

  const keyword = input.value.trim();

  // キーワードでフィルタリング
  const filtered = articles.filter((article) =>
    article.includes(keyword)
  );

  // 結果を表示
  results.innerHTML = '';
  filtered.forEach((article) => {
    const li = document.createElement('li');
    li.textContent = article;
    results.appendChild(li);
  });
});

リアルタイムフィルタリング

input イベントを使えば、送信ボタンなしでリアルタイムにフィルタリングできます。

input.addEventListener('input', (e) => {
  const keyword = e.target.value.trim();

  // 空のとき全件表示
  const filtered = keyword
    ? articles.filter((a) => a.includes(keyword))
    : articles;

  // 結果を更新
  results.innerHTML = '';
  filtered.forEach((article) => {
    const li = document.createElement('li');
    li.textContent = article;
    results.appendChild(li);
  });
});

フォームの値を取得する方法

<form id="my-form">
  <input type="text" name="username" id="username">
  <input type="email" name="email" id="email">
  <select name="role" id="role">
    <option value="admin">管理者</option>
    <option value="user">一般ユーザー</option>
  </select>
  <input type="checkbox" name="agree" id="agree">
  <button type="submit">送信</button>
</form>
document.querySelector('#my-form').addEventListener('submit', (e) => {
  e.preventDefault();

  // 方法1: querySelectorで個別に取得
  const username = document.querySelector('#username').value;
  const email = document.querySelector('#email').value;
  const role = document.querySelector('#role').value;
  const agree = document.querySelector('#agree').checked; // チェックボックスはchecked

  // 方法2: FormDataオブジェクトを使う(モダンな方法)
  const formData = new FormData(e.target);
  console.log(formData.get('username'));
  console.log(formData.get('email'));

  console.log({ username, email, role, agree });
});

removeEventListener

登録したリスナーを解除するときは removeEventListener を使います。

// 名前付き関数で定義する(必須)
function handleClick() {
  console.log('クリックされた');
}

const btn = document.querySelector('#btn');

// リスナーを登録
btn.addEventListener('click', handleClick);

// リスナーを解除(同じ関数の参照を渡す必要がある)
btn.removeEventListener('click', handleClick);
⚠️ 匿名関数は removeEventListener できない

removeEventListener は登録時と全く同じ関数の参照を渡す必要があります。アロー関数などの匿名関数は毎回別のオブジェクトとして作られるため、解除できません。

// これは解除できない!
btn.addEventListener('click', () => { console.log('クリック'); });
btn.removeEventListener('click', () => { console.log('クリック'); }); // 別の関数オブジェクト

// これは解除できる(同じ変数を参照している)
const fn = () => { console.log('クリック'); };
btn.addEventListener('click', fn);
btn.removeEventListener('click', fn); // 同じ fn を渡す

リスナーのクリーンアップが必要な場合(例:ページ内のコンポーネントを動的に削除するとき)は、必ず名前付き関数または変数に格納したアロー関数を使いましょう。

✍ やってみよう:カウンターとダークモード切り替え

2つのインタラクティブなUIを作ってみましょう。

課題 (a):クリックカウンター

ボタンをクリックするたびに数字が増えるカウンターを作ってください。

<!-- HTML -->
<div id="counter-app">
  <p>クリック回数: <span id="count-display">0</span></p>
  <button id="count-btn">クリック!</button>
  <button id="reset-btn">リセット</button>
</div>
// JavaScript のヒント
let count = 0;
const display = document.querySelector('#count-display');

document.querySelector('#count-btn').addEventListener('click', () => {
  count++;
  display.textContent = count;
});

// リセットボタンも実装してみよう
document.querySelector('#reset-btn').addEventListener('click', () => {
  // ここを埋めよう
});

課題 (b):ダークモード切り替え

ボタンをクリックするたびにダークモードとライトモードを切り替えてください。document.bodydark-mode クラスをトグルします。

<!-- HTML -->
<body>
  <button id="theme-btn">ダークモードに切り替え</button>
  <h1>見出しのテキスト</h1>
  <p>本文のテキストです。</p>
</body>
/* CSS */
body { background: #ffffff; color: #1a1a1a; transition: all 0.3s; }
body.dark-mode { background: #1a1a1a; color: #ffffff; }
// JavaScript のヒント
const themeBtn = document.querySelector('#theme-btn');

themeBtn.addEventListener('click', () => {
  // document.body の dark-mode クラスをトグル
  document.body.classList.toggle('dark-mode');

  // ボタンのテキストも切り替えよう
  const isDark = document.body.classList.contains('dark-mode');
  themeBtn.textContent = isDark ? 'ライトモードに切り替え' : 'ダークモードに切り替え';
});

発展課題: ダークモードの状態を localStorage に保存して、ページをリロードしても状態が維持されるようにしてみましょう。

// localStorage に保存
localStorage.setItem('darkMode', 'true');

// localStorage から読み込み
const savedMode = localStorage.getItem('darkMode');

まとめ

イベント処理のポイントをまとめます:

次のチャプターでは非同期処理と Fetch API を学びます。サーバーからデータを取得してDOMに反映させる、モダンなWebアプリの核心部分です。