Linkers Tech Blog

リンカーズ株式会社の開発者ブログです。

JavaScriptでのイベントキャンセルの落とし穴

Railsで開発していると、デフォルトのSubmitイベントを上書きしたい時に遭遇することがあると思います。 そういった場合は、Clickイベントなどを利用して、デフォルトのSubmitイベントはキャンセルし、上書きした処理を実行することになります。 しかし、イベントリスナーの設定方法やキャンセル方法によっては、予期せぬ動作を引き起こすことがあります。 JavaScriptのイベント伝播の仕組みを理解していれば、予期せぬ動作を未然に防ぐことが出来ます。

イベントの伝播の仕組み

JavaScriptでは、イベントは3つのフェーズで伝播していき、イベントをイベントリスナーが受け取ることでコールバック関数が実行されます。

  1. 最も外側にあるwindow要素からイベントターゲットとなる要素へと伝播していくキャプチャフェーズ
  2. イベントターゲット(イベントの発火元)となる要素を特定するターゲットフェーズ
  3. イベントターゲットからwindow要素に向かって伝播していくバブリングフェーズ

これらのフェーズについては、W3Cのドキュメントにある図がわかりやすいです。 event_flow 引用元: UI Events

デフォルトアクションのキャンセルとバブリングの停止

イベントリスナーの登録は、addEventListener()やjQueryのon()メソッドで行うことが多いと思います。 JavaScriptやjQueryのイベントオブジェクトには、イベントを受け取った要素のデフォルトアクション(a要素の画面遷移など)をキャンセルしたり、バブリングを停止したりするためのメソッドが用意されています。 イベントリスナーのコールバック関数でreturn falseすると、自動的にそれらのメソッドが呼び出されます。 メソッドの詳細は割愛しますが、メソッドの一覧とそれぞれの動作は下記の通りです。

メソッド デフォルトアクションの
キャンセル
バブリングの停止 同一要素内の
他のリスナーの停止
preventDefault() する しない しない
stopPropagation() しない する しない
stopImmediatePropagation() しない する する

ライブラリを使用しない通常のJavaScriptとjQueryとで、return falseした時に呼ばれるメソッドは下記の通りです。

メソッド 通常のJS jQuery
preventDefault()
stopPropagation() ×
stopImmediatePropagation() × ×

以上から、通常のJavaScriptとjQueryで return false した時の挙動をまとめると以下の通りになります。

デフォルトアクションの
キャンセル
バブリングの停止 同一要素内の
他のリスナーの停止
通常のJS キャンセルする 停止しない 停止しない
jQuery キャンセルする 停止する 停止しない

通常のJavaScriptではコールバック関数内でreturn falseしてもデフォルトアクション(フォームのsubmitなど)がキャンセルされるだけなのに対して、jQueryではデフォルトアクションのキャンセルに加え、 バブリングも停止 してしまいます。

この違いを知らずにコールバック関数内でreturn falseすると、伝播して欲しいイベントが伝播しなかったり、逆に伝播を停止したいイベントが伝播してしまったりします。

例:二重送信の防止

下記のようなフォームがあったとします。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
    <script src="./script.js" type="text/javascript"></script>
  </head>
  <body>
    <form name="sampleForm">
      <button type="submit">Submit</button>
    </form>
  </body>
</html>

ダブルクリックなどでフォームが二重に送信されることを防ぐために、サブミットボタンにdisabled属性を付与するケースは多々あると思います。 これを実現するために、button要素がクリックされた際に、disabled属性を付与するイベントリスナーをdocument要素に登録しました。

$(function(){
  $(document).on('click', 'button[type="submit"]', function() {
    $(this).prop('disabled', true);
  });
});

ここで、フォームの送信をJavaScriptから実施するように、イベントリスナーを登録してみましょう。 二重送信を防ぐため、ボタンのデフォルトアクション(submit)はキャンセルする必要があります。

$(function(){
  $('button[type="submit"]').on('click', function(event) {
    $('form').submit();
    event.preventDefault();
  })

  $(document).on('click', 'button[type="submit"]', function() {
    $(this).prop('disabled', true);
  });
});

注意したいのは、送信のキャンセルをreturn falseではなく、 event.preventDefault()で実行すること です。 jQueryでは、return falseはイベントの伝播も停止してしまうため、document要素のイベントが処理されず、button要素に二重送信防止のdisabled属性が付与されません。

なお、下記のようにaddEventListener()でイベントリスナーを登録すれば、return falseでフォームの送信のみキャンセルすることも可能です。 しかし、jQueryとのreturn falseの動作の違いを把握していなくてはならないため、デフォルトアクションをキャンセルする目的であれば、preventDefault()を利用した方が良いでしょう。

$(function(){
  dosument.querySelector('button[type="submit"]').addEventListener('click', function(event) {
    document.sampleForm.submit();
    // preventDefault()は呼ばれるがstopPropagation()は呼ばれないので、期待した通りの動作になる
    return false;
  })

  $(document).on('click', 'button[type="submit"]', function() {
    $(this).prop('disabled', true);
  });
});

まとめ

デフォルトアクションをキャンセルする場合はpreventDefault()、バブリングを停止する場合にはstopPropagation()といったように、目的に合わせてイベントオブジェクトのメソッドを使い分けましょう。 そうすることで、イベントリスナーの登録方法を気にすることなく、ソースコード上でも、何をキャンセルしているかを明示することができます。 jQueryを使用していてイベントの伝播も含めて停止したい場合であっても、preventDefault()stopPropagation()を利用することで、イベントの伝播を停止するという意図が明確になります。

(編集部註) 当初Slackの導入について執筆する予定でしたが、諸事情により弊社高田による記事をお送りしました。