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
- ボタン要素
大まかにですが、このコードで行っている処理は以下の流れです。
- ルート要素とリスト要素のイベントリスナーに同一のカスタムイベント(
clickBtnEvent
)を登録 - ボタンを押下した際にボタン要素からカスタムイベントを発火
- イベントのバブリングが行われ、ルート要素とリスト要素に登録されたカスタムイベント発火時の処理が実行される
では、細かく処理を説明していきます。
ルート要素とリスト要素のイベントリスナーに同一のカスタムイベント(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
を使用する際はバブリングを有効に使えないかを考えてみてはいかがでしょうか。