2019.10.14

JavaScript中級編!?カスタムイベントを使おう


この記事では、JavaScript のカスタムイベントについて紹介します。

タイトルに「中級編!?」とありますが、文法自体は全然むずかしくないです。ただ、入門書などではあまり触れられていないようです。

イベント自体がとても JavaScript らしい機能ですし、カスタムイベントは覚えておくとここぞというときにプログラムの表現の幅は広がるので、チェックして損はありません。

イベントとは

イベントは基本的に HTML 要素から発行されます。一番馴染みのあるイベントは「クリック」でしょう。

const buttonElem = document.querySelector('.button');

buttonElem.addEventListener('click', function(event) {
  // 第一引数はイベントに関する情報が詰まったオブジェクト
  // たとえばevent.targetにはイベントの発行元HTML要素が入っている
});

上記のコードで起こっていることは…

  • 前提として、ボタン要素(に限りませんが)は、クリックされたときに click という名前のイベントを発行します。
  • querySelector で取得した特定のボタン要素について、addEventListener メソッドで、click イベントに「イベントリスナー(イベントハンドラー)」を登録しています。
  • イベントリスナーとは、イベントが発行されたときに呼び出される関数です。
  • buttonElem から click イベントが発行されたとき、リスナーとして登録した関数が実行されます。

これは JavaScript の基本中の基本なので、みなさんお分かりかもしれませんね。

イベントは、どの要素からも発行されうる汎用的なものと、特定の要素からしか発行されないものに区別することができます。

clickmouseover などはほぼすべての要素から発行されます(どんな要素でもクリックはできる)。これに対し、以下のように、たとえば change イベントはフォームの入力項目などからしか発行されません。

inputElement.addEventListener('change', function() {/* ... */});
imageElement.addEventListener('load', function() {/* ... */});
videoElement.addEventListener('play', function() {/* ... */});

さらに以下のようなイベントは、DOM の構築を完了したときやコンテンツをダウンロードし終わったときにブラウザから自動的に発行されます。

document.addEventListener('DOMContentLoaded', function() {/* ... */});
window.addEventListener('load', function() {/* ... */});

カスタムイベント概要

さて、カスタムイベントとは、ユーザーが独自定義するイベントです。clickchange のようなイベントを、自由に作成できるということです。

最初から JavaScript に定義されているイベントでは、「どの要素から」「どのタイミングで」「どのような名前の」イベントが発行されるか決まっていましたが、カスタムイベントではそれらを独自に定義します。

イベントの作成

まずはイベントの作成方法です。CustomEvent オブジェクトをインスタンス化します。

const event = new CustomEvent('eventname');

コンストラクタの引数に、任意のイベント名を指定します。イベント名とは、clickchange に当たります。

イベントの発行

作成したイベントを特定の HTML 要素から発行するには、dispatchEvent メソッドを呼び出します。言ってみればボタンからクリックイベントを発行する部分です。

elem.dispatchEvent(event);

このような処理は、デフォルトのイベント処理では行いませんね。ブラウザによって裏側で行われるからです。カスタムイベントの場合は、自らの手で発行処理を書く必要があります。

イベントリスナー

発行されたイベントを待ち受けて処理を行う方法は、デフォルトのイベントと同じです。

elem.addEventListener('eventname', function() {
  // ...
});

addEventListener の第一引数のイベント名は、new CustomEvent に渡した名前です。

データの受け渡し

イベントにデータを含めることも可能です。CustomEvent コンストラクタの第二引数に指定します。detail というプロパティ名は決まっているようです。

const event = new CustomEvent('eventname', { detail: 'Hello world' });

elem.addEventListener('eventname', function(e) {
  console.log(e.detail);
});

elem.dispatchEvent(event); // -> 'Hello world'

イベントに追加されたデータは、上記のようにリスナー内で参照できます。

カスタムイベントのシンタックスはこれだけです。難しいというほどでもないですよね。

使いどころ

では、カスタムイベントにはどのようなユースケースが考えられるでしょうか?

カスタムイベントによって、コンポーネント同士の連携を、それぞれの独立性を保ったまま表現できます。

ここでいうコンポーネントとは、メニューやモーダルやタブなどを制御するプログラムです。たとえば、「メニューAが開いたときに、メニューBは自動的に閉じる」など、コンポーネント間の連携が必要な要件ってありますよね。ここでAの処理中にBについての処理を埋め込んでしまうと、プログラムの見通しが悪くなりがちです。このような場合に、カスタムイベントの使用を検討するとよいでしょう。

Aからイベントを発行して、Bがそのイベントをリッスンします。こういう構造にすることで、AとBの結びつきを最小限に抑えられます。Aからすると独自のイベントを発行しているだけなので、それをもって誰が何をしようと無関心でいられます。Bとしても、Aの内部実装が多少変わったところで関係なく、欲しいイベントさえ発行してくれればOKとなります。

以下にプログラム例を示します。もちろん完全なプログラムではなく、説明に必要な箇所だけの擬似的なコードです。ModalMenu という二つのコンポーネントがあり、「モーダルが開いたときはメニューは閉じる」という仕様があるとします。

Modal.js
function Modal(targetModalId) {
  this.modal = document.getElementById(targetModalId)
}

Modal.prototype = {
  open() {
    // モーダルを開く処理が書いてあるつもり

    // イベント発行
    const event = new CustomEvent('modalopen');
    this.modal.dispatchEvent(event);
  }
};

Modal.init = function() {
  // モーダルコンポーネントには.js-modalクラスが書かれる決まりとする
  Array.prototype.forEach.call(
    document.querySelectorAll('.js-modal'),
    elem => new Modal(elem.id)
  );
};
Menu.js
function Menu() {
  this.handleModalOpen();
}

Menu.prototype = {
  close() {
    // メニューを閉じる処理が書いてあるつもり
  },
  handleModalOpen() {
    const modals = document.querySelectorAll('.js-modal');

    Array.prototype.forEach.call(modals, modal => {
      const that = this;
      // モーダルが開いたときにメニューを閉じる
      modal.addEventListener('modalopen', function onOpen() {
        // イベントリスナーを削除しないと、
        // モーダルが開くたびに同じ処理を行うリスナーが追加されてしまう
        // addEventListenerでリスナーに名前をつけて、削除する関数に指定する
        modal.removeEventListener('modalopen', onOpen);
        // 自身の閉じる処理を呼び出す
        that.close();
      });
    });
  }
};

カスタムイベントを使用する雰囲気は掴んでいただけたでしょうか。

ポリフィル

CustomEvent は、IE では実装されていません。IE9 以上であれば、こちらのポリフィルを読み込むことで CustomEvent を使えるようになります。


以上、JavaScript のカスタムイベントについて紹介しました。コンポーネント間の連携がどうにもこんがらがってしまう、という場合は、カスタムイベントで綺麗にならないか、検討してみましょう。