プログラミング学習ノート

プログラミング学習の記録用です。

イベントバブリングの解消

実現したいこと

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でイベントリスナーを設定した要素はその子孫要素に対しても影響を及ぼす。