●イベントのバブリングとは?
JavaScriptでWebアプリケーションを開発していると、イベントのバブリングという現象に遭遇することがあります。
イベントのバブリングとは、ある要素で発生したイベントが、その要素の親要素、さらにその親要素へと順番に伝播していく現象のことを指します。
このバブリングは、意図しない動作を引き起こすことがあるため、時にはバブリングを停止したいと思うこともあるでしょう。
しかし、バブリングの仕組みをきちんと理解していないと、うまくコントロールできないかもしれません。
そこで本記事では、バブリングの具体例を交えながら、stopPropagationやpreventDefaultを使ったバブリングの停止方法について詳しく解説していきます。
JavaScriptのイベント処理の基礎から応用まで、ストーリー性を持って順序立てて説明するので、きっとスッと頭に入ってくるはずです。
イベントのバブリングをマスターすれば、より効率的で可読性の高いコードが書けるようになるでしょう。実務でも自信を持ってイベント処理を行えるようになること間違いなしです。
それでは、早速バブリングの具体例から見ていきましょう。
○バブリングの具体例
バブリングを理解するために、具体例を見てみましょう。
次のようなHTMLがあるとします。
<div id="parent">
<button id="child">クリック</button>
</div>
ここで、buttonとdivの両方にclickイベントのリスナーを設定します。
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', function() {
console.log('親要素がクリックされました');
});
child.addEventListener('click', function() {
console.log('子要素がクリックされました');
});
この状態でbuttonをクリックすると、コンソールには次のようなログが出力されます。
子要素がクリックされました
親要素がクリックされました
子要素のbuttonをクリックしたのに、親要素のdivのclickイベントも発火しています。
これがまさにイベントのバブリングです。
buttonで発生したclickイベントが、親要素のdivにも伝播しているのがわかります。
では、このバブリングを確認するサンプルコードを見てみましょう。
○サンプルコード1:バブリングの確認
<div id="outer">
<div id="inner">
<button id="button">クリック</button>
</div>
</div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const button = document.getElementById('button');
outer.addEventListener('click', function() {
console.log('outerがクリックされました');
});
inner.addEventListener('click', function() {
console.log('innerがクリックされました');
});
button.addEventListener('click', function() {
console.log('buttonがクリックされました');
});
実行結果
buttonがクリックされました
innerがクリックされました
outerがクリックされました
buttonをクリックすると、イベントがbuttonから順番にinner、outerへと伝播していくのがわかります。
これが典型的なイベントのバブリングの例です。
このようにイベントがネストした要素を上に向かって伝播していく様子を、あたかも水の泡が下から上に向かって上がっていくように見えることから、”バブリング”と呼ばれています。
しかし、時にはこのバブリングが邪魔になることもあります。
意図しない動作を引き起こしてしまうかもしれません。
そういった場面では、バブリングを停止したいと思うでしょう。
次は、stopPropagationメソッドを使って、バブリングを停止する方法を見ていきましょう。
●stopPropagationでバブリングを停止する
先ほどの例では、buttonをクリックするとイベントがどんどん上の要素に伝播していきました。
しかし、時にはこのバブリングを途中で停止したいこともあるでしょう。
そんな時に使えるのが、stopPropagationメソッドです。
stopPropagationは、イベントオブジェクトのメソッドの一つで、そのイベントがさらに親要素に伝播するのを防ぐことができます。
バブリングを停止したい要素のイベントリスナー内で、このメソッドを呼び出すだけでOKです。
では早速、stopPropagationの基本的な使い方から見ていきましょう。
きっと、イベントのバブリングと停止の仕組みが腑に落ちるはずです。
○サンプルコード2:stopPropagationの基本
先ほどのサンプルコードを少し修正して、buttonのclickイベントでstopPropagationを呼び出してみます。
<div id="outer">
<div id="inner">
<button id="button">クリック</button>
</div>
</div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const button = document.getElementById('button');
outer.addEventListener('click', function() {
console.log('outerがクリックされました');
});
inner.addEventListener('click', function() {
console.log('innerがクリックされました');
});
button.addEventListener('click', function(event) {
console.log('buttonがクリックされました');
event.stopPropagation();
});
実行結果
buttonがクリックされました
buttonのclickイベントで、event.stopPropagation()を呼び出したことで、イベントがそれ以上親要素に伝播しなくなりました。
innerとouterのclickイベントは発火していません。
stopPropagationは、まさにイベントのバブリングを停止するためのメソッドなのです。
ただ、stopPropagationを使う時は、本当にバブリングを停止して良いのか、よく考える必要があります。
意図しない動作を防ぐために、安易にstopPropagationを使うのはおすすめできません。
バブリングを停止することで、他の部分のイベント処理に影響を与えてしまうかもしれないからです。
○サンプルコード3:特定の要素のみバブリングを停止
では、特定の要素だけバブリングを停止したい場合はどうすれば良いのでしょうか。
その場合は、stopPropagationを条件分岐と組み合わせるのが有効です。
<div id="outer">
<div id="inner">
<button id="button">クリック</button>
</div>
</div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const button = document.getElementById('button');
outer.addEventListener('click', function() {
console.log('outerがクリックされました');
});
inner.addEventListener('click', function(event) {
console.log('innerがクリックされました');
if (event.target === button) {
event.stopPropagation();
}
});
button.addEventListener('click', function() {
console.log('buttonがクリックされました');
});
実行結果
buttonがクリックされました
innerがクリックされました
今度は、innerのclickイベントの中で、クリックされた要素(event.target)がbuttonだった場合のみ、stopPropagationを呼び出すようにしました。
こうすることで、button以外の部分をクリックした時は、outerまでイベントが伝播します。
buttonをクリックした時だけ、inner止まりになるわけですね。
このように、stopPropagationは使い方次第で、より細かい制御ができるのです。
でも、使いすぎには注意が必要ですよ。
バブリングを停止しすぎると、かえってコードが複雑になってしまうかもしれません。
○サンプルコード4:イベントデリゲートでの使用例
最後に、stopPropagationのもう一つの使い方として、イベントデリゲートパターンを見てみましょう。
イベントデリゲートとは、親要素にイベントリスナーを設定し、子要素で発生したイベントを親要素で処理する手法のことです。
子要素が動的に追加・削除される場合に、とても有効なパターンですね。
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
const list = document.getElementById('list');
list.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log(event.target.textContent);
event.stopPropagation();
}
});
document.body.addEventListener('click', function() {
console.log('bodyがクリックされました');
});
実行結果(Item 2をクリック):
Item 2
ここでは、ulに対してclickイベントを設定し、クリックされた要素がliだった場合に、そのテキストをログ出力しています。
そして、event.stopPropagation()でバブリングを停止しているのがポイントです。
これにより、li要素をクリックしても、bodyまでイベントが伝播しなくなります。
liのクリックイベントだけを独立して処理できるというわけですね。
イベントデリゲートパターンでは、このようにstopPropagationをうまく使うことで、より効率的なイベント処理が実現できます。
●preventDefaultでイベントの既定の動作を防ぐ
イベントのバブリングを停止する方法については理解が深まったと思います。
でも、JavaScriptのイベント処理には、もう一つ重要な概念があるんですよね。
それが、イベントの既定の動作をキャンセルするpreventDefaultメソッドです。
preventDefaultは、いわばイベントに対する「ちょっと待った!」という感じでしょうか。
例えば、フォームの送信ボタンをクリックした時の画面遷移や、リンクをクリックした時の画面遷移など、ブラウザが標準で行う動作をキャンセルすることができるのです。
これは、JavaScriptでフォームのバリデーションを行ったり、リンクをクリックした時に確認ダイアログを表示したりする場合などに、とても便利な機能ですよね。
ただ、preventDefaultの使い方を誤ると、ユーザーの意図しない動作になってしまう可能性もあります。
ですから、正しい使い方をきちんと理解しておくことが大切なのです。
それでは、サンプルコードを交えながら、preventDefaultの基本的な使い方から見ていきましょう。
きっと、イベント処理の理解がさらに深まるはずですよ。
○サンプルコード5:preventDefaultの基本
まずは、preventDefaultの基本的な使い方を見てみましょう。
リンクのクリックイベントを例に取ります。
<a href="https://www.example.com" id="link">リンク</a>
const link = document.getElementById('link');
link.addEventListener('click', function(event) {
event.preventDefault();
console.log('リンクがクリックされました');
});
実行結果(リンクをクリック)
リンクがクリックされました
ここでは、リンクのclickイベントの中で、event.preventDefault()を呼び出しています。
これにより、リンクをクリックしてもページ遷移が起こらなくなります。
代わりに、コンソールにメッセージが出力されるようになりました。
このように、preventDefaultを使うと、イベントに紐づけられた標準の動作をキャンセルできるのです。
ただし、必要以上に使用すると、ユーザーを混乱させてしまうかもしれません。十分注意が必要ですね。
○サンプルコード6:フォーム送信を防ぐ
次に、よくあるユースケースとして、フォームの送信を防ぐ例を見てみましょう。
<form id="myForm">
<input type="text" name="username" placeholder="ユーザー名">
<button type="submit">送信</button>
</form>
const form = document.getElementById('myForm');
form.addEventListener('submit', function(event) {
event.preventDefault();
const username = form.elements.username.value;
console.log('ユーザー名: ' + username);
});
実行結果(フォームを送信)
ユーザー名: [入力されたユーザー名]
ここでは、フォームのsubmitイベントでpreventDefaultを呼び出すことで、フォームの送信を防いでいます。
そして、フォームの内容を取得して、コンソールに出力しているんですね。
これは、フォームのバリデーションを行う際などに、よく使われるパターンです。
フォームの内容をJavaScriptで確認し、問題がなければAjaxで送信するなんてこともできますね。
ただし、フォームの送信を完全に防いでしまうと、JavaScriptが無効になっている環境では問題が起こります。
そういった場合のフォールバック処理も忘れずに用意しておきましょう。
○サンプルコード7:リンクのデフォルト動作を防ぐ
最後に、リンクのデフォルト動作を防ぐ例も見ておきましょう。
これは、リンクをクリックした時の確認ダイアログなどで使われます。
<a href="https://www.example.com" id="link">外部リンク</a>
const link = document.getElementById('link');
link.addEventListener('click', function(event) {
event.preventDefault();
const shouldProceed = confirm('本当にこのサイトを開きますか?');
if (shouldProceed) {
window.location.href = link.href;
}
});
実行結果(リンクをクリック)
[確認ダイアログが表示される]
[OKをクリックするとページ遷移、キャンセルをクリックすると何も起こらない]
ここでは、リンクのclickイベントでpreventDefaultを呼び出し、ページ遷移を一旦キャンセルしています。
そして、confirmメソッドで確認ダイアログを表示し、ユーザーの選択に応じて遷移するかどうかを決めています。
このように、preventDefaultとJavaScriptを組み合わせることで、より柔軟なインタラクションを実装できるのです。
ただし、ユーザーの期待する動作を妨げないよう、十分な配慮が必要ですよ。
●よくあるエラーと対処法
stopPropagationやpreventDefaultの使い方について理解が深まったところで、実際にコードを書いてみると、思わぬエラーに遭遇することもあるかもしれません。
特に、JavaScriptのイベント処理は、ちょっとややこしい部分があるので、つまずきやすいポイントがいくつかあるんですよね。
でも、エラーに遭遇した時こそ、本当の理解が深まるチャンスです。
エラーメッセージをじっくり読んで、なぜそのエラーが起きたのかを考えることが大切ですね。
そうすることで、より堅牢なコードが書けるようになるはずです。
それでは、イベント処理でよく遭遇するエラーと、その対処法を具体的に見ていきましょう。
きっと、みなさんも経験したことがあるような内容ばかりだと思います。
一緒に、エラーを乗り越えるコツをマスターしていきましょう。
○stopPropagationが効かない場合
まず、よくあるエラーの一つが、「stopPropagationが効かない」というものです。
コードは合っているはずなのに、バブリングが止まらない…。
そんな経験、ありませんか?
stopPropagationが効かない主な原因は、大きく分けて2つあります。
1つ目は、イベントリスナーの設定が「キャプチャリングフェーズ」になっている場合です。
stopPropagationは、バブリングフェーズでのみ有効で、キャプチャリングフェーズでは効きません。
// キャプチャリングフェーズでイベントリスナーを設定
element.addEventListener('click', function(event) {
event.stopPropagation();
}, true);
上記のように、addEventListenerの第3引数をtrueにすると、キャプチャリングフェーズでイベントリスナーが登録されます。
この場合、stopPropagationを呼び出しても、バブリングは止まりません。
2つ目は、同じ要素に複数のイベントリスナーが設定されている場合です。
stopPropagationは、そのイベントリスナー内でのバブリングは止められますが、他のイベントリスナーには影響を与えません。
element.addEventListener('click', function(event) {
event.stopPropagation();
});
element.addEventListener('click', function(event) {
console.log('これは実行される');
});
このように、同じ要素に複数のclickイベントリスナーが設定されている場合、stopPropagationを呼び出しても、2つ目のイベントリスナーは実行されてしまいます。
これらの問題を避けるためには、イベントリスナーの設定を見直し、意図しない動作になっていないかをチェックすることが大切です。
キャプチャリングフェーズを使う必要がない場合は、addEventListenerの第3引数は省略(またはfalse)にしましょう。
また、同じ要素には、なるべく一つのイベントリスナーだけを設定するようにしましょう。
○preventDefaultとreturn falseの違い
次によくあるのが、「preventDefaultとreturn falseの違いがわからない」というものです。
この2つは、どちらもイベントのデフォルト動作をキャンセルするために使われますが、動作に少し違いがあるんですよ。
return falseは、jQuery時代によく使われていた手法です。
イベントハンドラの返り値をfalseにすることで、イベントのデフォルト動作をキャンセルできました。
element.onclick = function() {
// 何か処理
return false;
};
ただし、return falseには2つの問題があります。
1つは、return falseではイベントのバブリングまで止めてしまうこと。
preventDefault()だけではバブリングは止まりませんが、return falseだとバブリングも止まってしまいます。
もう1つは、return falseがaddEventListener()で設定したイベントリスナーでは機能しないこと。
addEventListener()の場合は、preventDefault()を使う必要があります。
element.addEventListener('click', function(event) {
// 何か処理
event.preventDefault();
});
ですので、モダンなJavaScriptでは、イベントのデフォルト動作をキャンセルしたい場合はpreventDefault()を使うのが一般的です。
return falseは、jQueryレガシーコードを扱う際に理解しておくと良いでしょう。
○イベントリスナーの重複登録に注意
最後に、イベントリスナーの重複登録についても触れておきましょう。
これは、同じ要素に同じイベントリスナーを複数回設定してしまうことで起こるエラーです。
function handleClick() {
console.log('クリックされました');
}
element.addEventListener('click', handleClick);
element.addEventListener('click', handleClick);
上記のコードでは、同じhandleClick関数が2回addEventListenerに渡されています。
この場合、クリックイベントが発生すると、handleClickが2回実行されてしまいます。
これを避けるためには、イベントリスナーを設定する前に、そのイベントリスナーがすでに設定されているかどうかをチェックする必要があります。
function handleClick() {
console.log('クリックされました');
}
if (!element.onclick) {
element.addEventListener('click', handleClick);
}
上記のように、element.onclickがfalsy(未設定)の場合だけaddEventListenerを呼び出すようにすれば、重複登録を防ぐことができます。
ただ、このようなチェックは手間がかかるので、そもそもイベントリスナーを複数回設定しないように気をつけるのが一番です。
コードの構造をシンプルに保ち、一つの処理は一つの場所で行うように心がけましょう。
さて、イベント処理のよくあるエラーについて、一通り見てきました。
エラーは学習のチャンスです。エラーに遭遇した時は、落ち着いて原因を特定し、適切に対処することが大切ですね。
そうすることで、より堅牢で可読性の高いコードが書けるようになるはずです。
みなさんも、これからのコーディングで、ぜひこれらのポイントを意識してみてくださいね。
●バブリング停止の応用例
さて、これまでイベントのバブリングと、それを停止する方法について詳しく見てきました。
stopPropagationやpreventDefaultの使い方も、だいぶ身についてきたのではないでしょうか。
でも、知識を身につけるだけでは不十分ですよね。大切なのは、その知識を実際の開発で活用できるようになることです。
せっかく学んだことを、実践で生かせないのではもったいない。
そこで、ここからは少し視点を変えて、バブリング停止を応用したより実践的な例を見ていきたいと思います。
モーダルウィンドウやネストしたメニューなど、実際のWebアプリでよく見かけるUIの実装に、stopPropagationをどう活かせるのかを考えていきましょう。
きっと、「あ、こんなところで使えるんだ!」と思うはずです。
それでは、具体的なコード例を交えながら、一緒に見ていきましょう。
○サンプルコード8:モーダルウィンドウ内のクリック
まずは、モーダルウィンドウの例から見てみましょう。
モーダルウィンドウとは、画面の中央に表示されるポップアップウィンドウのことです。
よくログインフォームや確認ダイアログなどで使われますね。
このモーダルウィンドウ内でクリックイベントが発生した時、それが意図せずモーダルの外側まで伝播してしまうと困ります。
そんな時は、stopPropagationを使ってバブリングを止めるのが有効です。
<div id="modal">
<div class="modal-content">
<p>これはモーダルウィンドウです</p>
<button id="closeButton">閉じる</button>
</div>
</div>
const modal = document.getElementById('modal');
const closeButton = document.getElementById('closeButton');
modal.addEventListener('click', function() {
modal.style.display = 'none';
});
closeButton.addEventListener('click', function(event) {
event.stopPropagation();
});
ここでは、モーダルウィンドウ自体(modal)にclickイベントリスナーを設定し、クリックされたらモーダルを閉じるようにしています。
一方、モーダル内の閉じるボタン(closeButton)のclickイベントでは、stopPropagationを呼び出しています。
これにより、閉じるボタンをクリックしても、イベントがモーダル自体に伝播しなくなります。
結果として、閉じるボタンをクリックしてもモーダルが閉じなくなるわけですね。
これは、閉じるボタンに別の処理(フォームのsubmitなど)を設定する場合などに便利です。
○サンプルコード9:ネストしたメニューでの使用
もう一つ、ネストしたメニューの例も見てみましょう。
メニュー項目の中に、サブメニューが含まれているようなケースです。
このような場合、サブメニューをクリックした時に、親メニューのクリックイベントまで発火してしまうと、意図しない動作になってしまいます。
ここでも、stopPropagationが活躍します。
<ul id="menu">
<li>メニュー1</li>
<li>メニュー2
<ul>
<li>サブメニュー2-1</li>
<li>サブメニュー2-2</li>
</ul>
</li>
</ul>
const menu = document.getElementById('menu');
menu.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('メニューがクリックされました');
}
});
const subMenus = document.querySelectorAll('#menu ul');
subMenus.forEach(function(subMenu) {
subMenu.addEventListener('click', function(event) {
event.stopPropagation();
if (event.target.tagName === 'LI') {
console.log('サブメニューがクリックされました');
}
});
});
ここでは、親メニュー(menu)とサブメニュー(subMenus)、それぞれにclickイベントリスナーを設定しています。
親メニューのイベントリスナーでは、クリックされた要素がLIだった場合に、「メニューがクリックされました」とログ出力するようにしています。
一方、サブメニューのイベントリスナーでは、最初にstopPropagationを呼び出しています。
これにより、サブメニューをクリックしても、イベントが親メニューに伝播しなくなります。
そのうえで、クリックされた要素がLIだった場合に、「サブメニューがクリックされました」とログ出力しています。
こうすることで、サブメニューをクリックしても、親メニューのクリックイベントは発火しなくなります。
ネストしたメニューを扱う際には、このテクニックが重宝しますよ。
○サンプルコード10:カスタムイベントでデータを渡す
最後に、少し発展的な話題として、カスタムイベントについても触れておきましょう。
カスタムイベントとは、私たちが独自に定義するイベントのことです。
stopPropagationは、このカスタムイベントでも活用できます。
特に、イベントと一緒にデータを渡したい場合に便利です。
const myEvent = new CustomEvent('myEvent', {
detail: { message: 'これは私のイベントです' },
bubbles: true,
cancelable: true
});
document.addEventListener('myEvent', function(event) {
console.log(event.detail.message);
});
const button = document.getElementById('button');
button.addEventListener('click', function(event) {
event.stopPropagation();
document.dispatchEvent(myEvent);
});
ここでは、まず「myEvent」というカスタムイベントを定義しています。
このイベントには、「message」というデータを含めています。
そして、document全体で「myEvent」のリスナーを設定し、イベントが発火したらそのデータ(event.detail.message)をログ出力するようにしています。
一方、ボタン(button)のclickイベントでは、stopPropagationを呼び出した後、document.dispatchEventを使って「myEvent」を発火させています。
こうすることで、ボタンをクリックした時に、クリックイベントのバブリングは止まりつつ、「myEvent」だけが独立して発火するようになります。
このように、stopPropagationとカスタムイベントを組み合わせることで、より柔軟なイベント制御が可能になるのです。
まとめ
JavaScriptのイベントバブリングについて、かなり深堀りしてきました。
バブリングの仕組みから、それを停止するstopPropagationやpreventDefaultの使い方まで、具体的なコード例を交えながら詳しく見てきましたね。
最初は少しややこしく感じたかもしれませんが、徐々にイメージがつかめてきたのではないでしょうか。
stopPropagationでバブリングを止める、preventDefaultでデフォルトの動作をキャンセルする…。
このテクニックを使いこなせば、よりインタラクティブなUIが実装できるはずです。
大切なのは、実際にコードを書いて試してみること。
サンプルコードを参考にしながら、自分の手を動かしてみてください。
エラーにぶつかったら、その原因を考え、解決方法を探る。
そうやって、一つ一つ乗り越えていくことが、本当の力につながります。