CustomEventをバブリングさせてみよう

UI開発者 大石

この記事はミツエーリンクス Advent Calendar 2020 - Adventarの3日目の記事です。

突然ですが、CustomEventを使っていますか?

CustomEventは、任意のイベントを定義することができます。ちょっと使用するのが難しい機能かもしれないですが、使い方を理解できるようになるととても便利な機能です。今回はこのCustomEventをちょっと踏み込んで、バブリングをさせてみたいと思います。

バブリングとは

イベントの発火した要素から祖先要素にさかのぼってイベントを伝搬させることです。このバブリングを使うとなにが便利かというと、「異なる要素間でのイベント伝搬が簡単になる」というところです。

具体的にコードを紹介しながら説明します。

ボタンを押下したら見出しテキストを変更する

「ボタンを押下した際にボタンの見た目を変更し、押したボタンのテキストを見出しテキストに表示する」という例で紹介します。まずはコードを見てみましょう。

<div id="root">
<h2 id="hdg"></h2>
<ul id="list">
<li><button class="btn" type="button">1</button></li>
<li><button class="btn" type="button">2</button></li>
<li><button class="btn" type="button">3</button></li>
<li><button class="btn" type="button">4</button></li>
<li><button class="btn" type="button">5</button></li>
</ul>
</div>
const root = document.querySelector('#root');
const hdg = root.querySelector('#hdg');
const list = root.querySelector('#list');
const btns = root.querySelectorAll('.btn');
const instanceList = [];
const event = new CustomEvent('clickBtnEvent', {
    bubbles: true
});

class Btn {
    constructor(btn) {
        this._btn = btn
        this._isSelect = false;
    }

    select() {
        this._btn.dispatchEvent(event);
        this._isSelect = true;
        this._btn.style.backgroundColor = 'blue';
        this._btn.style.color = '#fff';
    }

    reset() {
        if (!this._isSelect) {
            return;
        }
        this._isSelect = false;
        this._btn.style.backgroundColor = '#efefef';
        this._btn.style.color = '#000';
    };

    init() {
        this._btn.addEventListener('click', this.select.bind(this));
    }
}

for(const btn of btns) {
    const instance = new Btn(btn);
    instance.init();
    instanceList.push(instance);
}

const hdgEvent = (e) => {
    hdg.textContent = `Select is ${e.target.textContent}`;
}

const btnEvent = (e) => {
    for (const instance of instanceList) {
        instance.reset();
    }
}

root.addEventListener('clickBtnEvent', hdgEvent);
list.addEventListener('clickBtnEvent', btnEvent);

文章中で表現する要素の内容は以下のように表現させていただきます。

  • div#root
    • ルート要素
  • h2#hdg
    • 見出し要素
  • ul#list
    • リスト要素
  • button.btn
    • ボタン要素

大まかにですが、このコードで行っている処理は以下の流れです。

  1. ルート要素とリスト要素のイベントリスナーに同一のカスタムイベント(clickBtnEvent)を登録
  2. ボタンを押下した際にボタン要素からカスタムイベントを発火
  3. イベントのバブリングが行われ、ルート要素とリスト要素に登録されたカスタムイベント発火時の処理が実行される

では、細かく処理を説明していきます。

ルート要素とリスト要素のイベントリスナーに同一のカスタムイベント(clickBtnEvent)を登録

まずはCustomEventの定義です。

CustomEventはコンストラクタの第2引数に{bubbles: true}を指定すると、バブリングを設定することができます。そして定義したCustomEventを要素に登録しますが、今回の狙いは2つです。

  • ボタン押下時にボタン要素の見た目を変更する
    • 押下したボタンを青、押下されなかったボタンを元に戻す
  • ボタン押下時にボタン要素のテキストを見出しに反映させる

「ボタン押下時にボタン要素の見た目を変更する」イベントを登録するのは、ボタン要素の親となるリスト要素です。各ボタン要素を管理するBtnクラスを定義し、ボタン要素の親要素であるリスト要素をメンバーに持たせ、カスタムイベントを登録します。リスト要素はカスタムイベントが発火されるのを待ち受けて、ボタン要素の見た目を元に戻すresetメソッドを実行します。

「ボタン押下時にボタン要素のテキストを見出しに反映させる」イベントを登録するのは、全体の親となるルート要素です。ルート要素にカスタムイベントを登録し、ボタン押下時にバブリングされたイベントをキャッチして見出しテキストの変更を行うhdgEvent関数を実行します。

ボタンを押下した際にボタン要素からカスタムイベントを発火

今回の機能のフックになるアクションは「ボタン押下」です。CustomEventはイベントの発火タイミングを自分で実装しないといけないので、ボタン押下時の処理であるselectメソッドの中でdispatchEventメソッドを実行します。こうすることで、ボタン押下時にカスタムイベントを発火させることができます。

ここで注目していただきたいのは、カスタムイベントを発火させるdispatchEventメソッドを呼び出すのはボタン要素自身ということです。今回はバブリングを設定しているので、イベントを発火させたボタン要素からリスト要素、ルート要素という流れでイベントが伝搬していきます。hdgEvent関数では見出しテキストの変更を行うため「どのボタン要素が押下されたのか」という情報が必要です。カスタムイベントのリスナー関数には引数としてイベントオブジェクト自体を受け取ることができ、そのイベントのtargetプロパティにはカスタムイベントを発火させた要素がセットされるので、ボタン要素自身からイベントを発火させる必要があるのです。

イベントのバブリングが行われ、ルート要素とリスト要素に登録されたカスタムイベント発火時の処理が実行される

カスタムイベントが発火したら以下の処理を行います。

  • Btnクラスのresetメソッドではボタン要素の見た目を元に戻す
  • hdgEvent関数では押下されたボタンを引数のeから参照し、見出しテキストを変更する

これで「ボタンを押下した際にボタンの見た目を変更し、押したボタンのテキストを見出しテキストに表示する」という目的を果たす機能が実装できました。バブリングを設定することでイベントの伝搬を容易にすることができ、結果コード量を減らし可読性の高いコードを書けるようになったのではないでしょうか。

まとめ

今回紹介したコードですが、バブリングを使わずともselectメソッドから見出しテキストを変更する処理を書くことで、同様の結果を得ることができます。しかし、そうするとボタン要素を管理しているはずのBtnクラスが本来管理していない見出し要素に対して変更を加えることになり、管理範囲が広がってしまいます。今回はとても小さな機能なのでそこまで問題にはならないですが、もっと大きな機能を開発することになるとこの問題がどんどん複雑になっていきます。そこでバブリングを使うことによってBtnクラスが管理している範囲をボタン要素に集中することができ、責任範囲を限定できるので、CustomEventを使用する際はバブリングを有効に使えないかを考えてみてはいかがでしょうか。