イベント処理
addEventListener、イベントオブジェクト、イベント委任、フォームイベントなど、インタラクティブなUIの基礎を学びます。
このチャプターで学ぶこと
- 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を使ったことがある方は $('#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 を使いましょう。
キーボード
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');
});
<script> タグをHTMLの <head> に書くと、HTML要素がまだ読み込まれていない状態でJavaScriptが実行されてしまい、querySelector が null を返すことがあります。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は仮想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 は登録時と全く同じ関数の参照を渡す必要があります。アロー関数などの匿名関数は毎回別のオブジェクトとして作られるため、解除できません。
// これは解除できない!
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.body に dark-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'); まとめ
イベント処理のポイントをまとめます:
- イベント登録:
element.addEventListener('イベント名', コールバック)が基本。インラインonclick=""は避ける - イベントオブジェクト: コールバックの第一引数
eで受け取る。e.targetでクリックされた要素、e.preventDefault()でデフォルト動作をキャンセル - よく使うイベント:
click,input,change,submit,keydown,focus,blur inputvschange:inputはリアルタイム発火、changeはフォーカスが外れたとき- バブリング: イベントは子→親と伝播する。
stopPropagationで止められる - イベント委任: 親要素にリスナーを登録して
e.targetで子を判別。動的に追加された要素にも対応できる - リスナーの解除:
removeEventListenerには名前付き関数が必要。匿名関数は解除できない
次のチャプターでは非同期処理と Fetch API を学びます。サーバーからデータを取得してDOMに反映させる、モダンなWebアプリの核心部分です。