はらへり日記

腹に弾丸

JSでカスタムイベントを作る

はじめに

qiita.com

この記事は株式会社アイスタイルアドベントカレンダーの1日目の記事です。

今日のお話

新卒研修でSinatraで作られた社内向けツールをLaravel5でフルスクラッチで改修するというものを行いました。

その際、フロントJSの実装はほぼ私が担当したのですが、その際に得たカスタムイベントという知見についてお話します。

JavaScriptでいうイベントとは

そもそもイベントとはなんぞやという話ですが、JavaScriptでは様々なイベントにフックして処理を行うことができます。

どういう場面で使うかというと、例えば「ユーザが"送信"ボタンをクリックした」イベントにフックして「alertを表示する」というようなことです。

コードで書くと以下のような感じですね。

HTML

<body>
  <button id="button">送信</button>
</body>

JavaScript

var onClick = function alertAdvent() {
  alert('アドベントカレンダー1日目だおლ(´ڡ`ლ)');
}

document.getElementById('button').addEventListner('click', onClick);

まぁよく見るコードですね!

他にもスクロールイベントやフォームでのSubmit等、様々なイベントがJavaScriptにはあらかじめ用意してあります。

研修でやりたかったこと

私自身、簡単なDOM操作であれば経験がありましたがきちんとJSを書くのはほぼ初めてだったので過去の経験やらを元に以下の様な方針で実装を進めていました。

  • フロントサイドのリッチなFWは導入しない(それほどの規模ではないため)
  • ファイルごとの責任を明確にする
    • DOM操作とAjaxをなるべく同居させない等
  • 1ファイルのサイズを小さく保つ
    • 100行以内を目指し、見通しをよくする

なので技術的構成としてはjqueryを中心としたピュアJSで書いた複数ファイルを browserifyで圧縮するという描画以外はほぼピュアJSに近い構成で開発を行いました。

ここで少し困ったことが起きました。

こんなことがしたい

以下の様な処理をしたい時がありました。

  • 画像をクリックする
  • 画像IDに応じたAjaxが飛ぶ / モーダルウィンドウが表示される
  • モーダルウィンドウにAjaxで取得した情報が表示される

これをざっくり表現すると以下のようになります。

$('.image').click(function() {
  var id = $(this).val();
  var data = null;
  // このrequestは外部ファイルから生成したAjaxラッパーのインスタンス
  request.getData(id).then(function(result) { data = result; });
  /* モーダルウィンドウ処理*/
});

この際、モーダルウィンドウ処理がかなりファットになってしまいこれを外部ファイルに切り出すことにしました。

しかしここで疑問が生じます。

$('.image')のclickイベントに対して行いたい処理がいくつかあるのであれば新しいファイルでも同じDOMのイベントをListenしなければなりません。

それなら関数化したいけどそれにしてはDOMと結合しすぎている処理でしたし、そもそもその作りだと他のイベントや場面でモーダルウィンドウを出したい時に使い回すことができず、それではただファイルを分けただけとなってしまいます。

そこで救世主、カスタムイベントです。

カスタムイベントとは

その名の通り、自分だけのイベントを定義し発火させることができます。

イベントには名前や発火時に渡すデータを定義することができ、これを上手く利用することによってファイル同士の結合を疎に近づけることができます。

今回はモーダルウィンドウを発火させるためのイベント、modalEnableを定義し、切り出したモーダルウィンドウのロジックはそれをListenすることにします。

それにより、どんな処理、どんなファイルからでもmodalEnableを発火させることでモーダルウィンドウを表示することが可能になります。

作り方

カスタムイベントの追加方法は以下の通りです。

// イベントを定義
var event = new Event('modalEnable');

簡単!後はイベントリスナーを当てて

// 'modalEnable'イベントを検知する
var button = document.getElementByTagName('button');
button.addEventListener('modalEnable', function() { alert('hello'); });

イベントを発火させるには以下のようにします。

// buttonに対し、event(modalEnable)を発火させる
button.dispatchEvent(event);

かなり簡単ですね!

先ほどの例のスクリプトを分離させ、カスタムイベントで繋いでみましょう。

app.js

$('.image').click(function() {
  var id = $(this).val();
  var data = null;
  // このrequestは外部ファイルから生成したAjaxラッパーのインスタンス
  request.getData(id)
    .then(function(result) { 
      data = result;
      var event = new Event('modalEnable'); // "modalEnable"イベントを生成
      document.body.dispatchEvent(event); // document.bodyに対して"modalEnable"を発火 
    });
});

modal.js

$(document).on('modalEnable', function() {
  /* モーダルウィンドウの処理 */
});

これでモーダルウィンドウ処理をどこからでも発火させられる形で別ファイルに分離できました!

僕はこの後、イベント生成部分、発火部分をさらにラップしてutilクラスとして別ファイルに切り出して管理しましたが、ここらへんは好みとその時の状況に合わせてよしなにやればいいと思います。

これで完璧やろ!俺天才!って思ってドヤ顔でpushしました。そしたら悲劇が起こりました。

悲劇

f:id:sota1235:20151201100849j:plain

安定のIE

まぁ、あれです。このコード、IEでは動きませんでした。

これが社会の洗礼ですねって思いながらIEでカスタムイベントを定義する方法を調べました。

結論

結論として、イベント生成部分を少しばかり凝らなければいけません。

のでJSでブラウザ判定の関数、isIe()あたりを定義してイベント生成関数を作りました!

(ブラウザ判定はua-parser-jsが便利です)

// make event obj
var makeEvent = function(eventName) {
  if(isIe()) {
    // create event when IE
    var evt = document.createEvent('Event');
    evt.initEvent(eventName, true, false);
    console.log(evt);
    return evt;
  }
  return new Event(eventName);
};

これによりIEでもそれ以外でもイベントオブジェクトを作成することができます!

イベント発火

イベント発火はIEでも同じ処理でいけるのですが、せっかくなのでメソッド化してみます

var fireEvent = function(dom, eventObj) {
  dom.dispatchEvent(eventObj);
};

シンプルですね。引数のDOMオブジェクトに対し、指定されたイベントを発火させます。

使い方

// あらかじめ'modalEnable'イベントを定義
var modalEnable = makeEvent('modalEnable');

// modalEnableイベントをid=modalのDOMに対して発火
fireEvent($('#modal'), modalEnable);

簡単!

まとめ

当時の僕の日誌の一言IEは滅べばいいと思います。僕の1時間を返せ。」

明日はktarow氏の「複数の言語を同時に触ることになった俺は」です!お楽しみに!