実現したいこと
TODOリストのうち、選択したタスクだけに取り消し線を引きたい。
問題
複数のタスクを作成すると、取り消し線が引かれるタスクと引かれないタスクがある。
ソースコード
// チェックを入れるとタスクに取り消し線を引く const checkboxes = document.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(function(checkbox){ checkbox.addEventListener('change', function(event){ const taskElement = event.target.parentNode.querySelector('span'); console.log(event.target); taskElement.classList.toggle('done'); }) });
原因
イベントバブリングが生じた。
イベントバブリングとは?
イベントバブリングは、DOM内でイベントが発生した要素から親要素へと順番に伝播する現象。
HTML要素のツリー構造によって生じるもので、イベントが発生した要素から親要素、その親要素の親要素へと順にイベントが伝わっていくため、複数の要素が同じイベントをキャッチすることができる。
今回起きたイベントバブリングの概要
今回のイベントのターゲット要素は<input type="checkbox">
。その親要素は<li>
。さらにその親要素は<ul>
。
本来はチェックを入れたタスクごと(<li>
ごと)に処理を実行したいのに、親要素の<ul>
にもイベントが伝播していたと考えられる。
解決法
イベントデリゲーションを使う。
イベントデリゲーションとは
親要素に1つのイベントリスナーを追加し、子要素で発生したイベントを親要素でキャッチし、その後、適切な子要素を特定して処理を行う手法。デリゲートは委任という意味。子要素の共通の親要素にイベントキャッチ処理を委任する。
//ulタグの要素を取得 const todoList = document.getElementById('todo-list'); // チェックを入れるとタスクに取り消し線を引く todoList.addEventListener('click', function(event){ if (event.target.tagName === 'INPUT' && event.target.type === 'checkbox') { const listItem = event.target.parentNode; listItem.classList.toggle('done'); } })
チェックボックスにチェックが入ったら(ifの条件)、チェックボックスの親要素である<li>
を取得して、クラス名「done」を設定。
CSSファイルでdoneクラスに取り消し線をしているので、チェックが入ると取り消し線が引かれる。
toggleメソッドは、クラス名がなければ新たに設定し、あれば変更するという便利なメソッド。
.done { text-decoration: line-through; }
学んだこと
- イベントデリゲーションは子とその直接の親じゃなくても設定できる。 子とその親の親でも可。
- addEventListenerは対象とするオブジェクトが必ずしもeventと同じとは限らない。
今回addEventListenerを<ul>
の要素ノードに指定しているが、eventのターゲットはチェックボックス。addEventListenerの対象となるオブジェクトが広い場合、eventがその子要素となることもある。addEventListenerでイベントリスナーを設定した要素はその子孫要素に対しても影響を及ぼす。