getElementByIdと同じ感覚で、querySelectorAllで複数の要素を取得してaddEventListenerを付けようとすると、なぜかイベントが動かない——JavaScriptを書き始めた頃によくつまずくポイントです。
結論から言うと、querySelectorAllが返すのは要素の集まり(NodeList)で、集まり自体にはaddEventListenerを付けられないのが原因です。解決策は、forEachでループし、1つずつの要素にイベントを付けることです。この記事では、単一要素の基本から、複数要素への正しい付け方までを解説します。
まずは基本から確認します。よく使うのは、getElementByIdで1つの要素を取得し、addEventListenerでイベントを付ける方法です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body>
<input type="button" id="button" value="ボタン">
</body>
</html>
// #buttonボタンをクリックしたときに、テキストを表示する
const button = document.getElementById("button");
button.addEventListener("click", function() {
console.log("ボタンがクリックされました");
});
getElementByIdは1つの要素そのものを返すため、そこへ直接addEventListenerを付けられます。ここまでは問題なく動きます。
ところが、同じ感覚でquerySelectorAllを使い、取得した複数要素にまとめてaddEventListenerを付けようとしても、うまくいきません。
理由は、querySelectorAllが返すのがNodeListという「要素の集まり」だからです。addEventListenerは個々の要素が持つメソッドであって、集まり全体に対して呼べるものではありません。そのため、NodeListに直接付けようとしても機能しないのです。では、複数の要素にイベントを付けるにはどうすればよいのでしょうか。
答えは、forEachです。querySelectorAllで取得したNodeListをforEachでループし、その中で1つずつの要素にaddEventListenerを付けます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body>
<div class="tab">タブ</div>
<div class="tab">タブ</div>
<div class="tab">タブ</div>
<div class="tab">タブ</div>
<div class="tab">タブ</div>
</body>
</html>
// .tabをクリックしたときに、テキストを表示する
const tabs = document.querySelectorAll("div.tab");
tabs.forEach(function(tab) {
tab.addEventListener("click", function() {
console.log("タブがクリックされました");
});
});
forEachのコールバックが受け取るtabが、1つずつの要素です。このtabに対してaddEventListenerを付けているので、5つの.tabすべてにクリックイベントが登録されます。これで、どのタブをクリックしても処理が実行されるようになります。
NodeListは配列によく似ていますが、厳密には配列ではありません。ただしforEachは使えるので、ループ処理はそのまま書けます。もしmapやfilterといった配列メソッドを使いたい場合は、Array.from()や[...nodeList]でいったん配列に変換してから扱います。
また、querySelectorAllで要素が1つも見つからなかった場合でも、返るのは空のNodeListです。forEachは空なら単にループが回らないだけなので、この点ではエラーになりにくく安心です。タブやボタンなど「同じクラスの要素が複数ある」UIを扱うときの基本パターンとして、覚えておくと重宝します。
ちなみにjQueryを使う場合は、$('.tab').on('click', ...)と書くだけで、内部で該当するすべての要素にイベントが登録されます。素のJavaScriptでforEachを回すのは、これと同じことを手動で行っているイメージです。両者を対比して捉えておくと、jQueryと素のJavaScriptを行き来するときに混乱しません。
さらに、要素が非常に多い場合や、あとから動的に追加される要素にも対応したい場合は、親要素にイベントを1つだけ付けて子要素を判定する「イベント委譲(デリゲーション)」という手法もあります。まずはforEachを基本に押さえ、必要に応じて使い分けるとよいでしょう。
getElementByIdは単一要素を返すので、直接addEventListenerを付けられるquerySelectorAllが返すNodeListは要素の集まりなので、直接は付けられないforEachでループし、1つずつの要素にaddEventListenerを付けるArray.from()で配列に変換する「単一要素」と「複数要素」で書き方が変わる、という点さえ押さえれば迷いません。複数の要素に同じ処理を付けたいときは、querySelectorAll+forEachをワンセットで思い出してください。