ITエンジニアによるITエンジニアのためのブログ

Rails UJSによるボタン無効化の仕組みと例外対応

HTMLフォームの送信時に、ユーザーが複数回送信ボタンをクリックしてしまうと、サーバー側で重複処理が実行されてしまう問題があります。Rails UJSはこれを自動的に無効化してくれる便利な機能ですが、特定のケースでは無効化されないことがあります。本稿では、Rails UJSによるボタン無効化の仕組みと、無効化されない場合の解決策について解説します。

Rails UJSによる二重送信防止

Rub on Railsには、Unobtrusive JavaScript(UJS)と呼ばれるJavaScriptライブラリが標準で搭載されており、フォームの二重送信を自動的に防止する機能を提供しています。

UJSは、特定のHTML要素にクリックイベントハンドラーを付与し、送信ボタンがクリックされた際にボタンを無効化することで(element.disabled = true)、二重送信を防ぎます。

UJSの仕組み

UJSは、以下のHTML要素を対象に二重送信防止機能を適用します。

対象要素
特定のdata属性を持つリンクa[data-confirm]
フォーム外にある、特定のdata属性を持つボタンbutton[data-remote]:not([form])
フォーム中の送信ボタンbutton[type=submit][form]、input[type=submit][form]

UJSの動作フローは以下の通りです。

  1. ユーザーが送信ボタンをクリックする。
  2. UJSが送信ボタンを無効化する。
  3. フォームが送信される。
  4. サーバー側で処理が完了する。

UJSの無効化リスト対象外

UJSは、button要素でtype属性がbuttonの場合、無効化の対象外となります。これは、HTMLの仕様でbutton要素のtype属性がbuttonの場合、フォーム送信が行われないため、二重送信の懸念がないことから、UJSによる無効化の対象外となります。

そもそも、ボタンのtype属性をsubmitからbuttonに変えるということは、ディベロッパーが通常のフォーム送信以外の挙動をカスタムJSを使って実現したいという明確な意図から行われるものなので、通常であれば問題は発生しません。

ただ、クリックイベントでカスタムJSによる中間処理をトリガーした後で最終的にフォーム送信を行なう場合、自身で送信ボタンを無効化しておかないと、ダブルクリックによるフォームの重複送信が行われてしまう恐れがありますので、注意が必要です。

例として、Railsでよく使われるstimulus.jsを使って、自身でボタンを無効化する処理を記載してみます。

まず、stimulus.jsのコントローラーを作成します。

import { Controller } from "stimulus";
import Rails from "@rails/ujs";

export default class extends Controller {
  static targets = ["customForm"];

  custom_submit(event) {
    /* 送信ボタンを無効化 */
    event.currentTarget.disabled = true;

    /* カスタム処理を実行 */
    if (this.perform_custom_job()) {
      /* 成功したらフォーム送信 */
      Rails.fire(this.customFormTarget, 'submit');
    } else {
      /* 失敗したら送信ボタンを再び有効化 */
      event.currentTarget.disabled = false;
    }
  }

  perform_custom_job() {
    return false;
  }
}

上記コードでは、custom_submitメソッドがボタンクリック用のイベントハンドラーになり、以下の処理を行っています。

  1. 重複クリックを防ぐためにまずはボタンを無効化。
  2. カスタムロジックを実行(perform_custom_jobメソッド)して、結果をリターン。
  3. 結果が成功だったらフォーム送信。失敗だったらもう一度ボタンをクリックできるように、ボタンを有効化。

そして、HTMLフォームでは以下のように記載し、フォームとコントローラーを連携します。

# HTML
<form data-controller="form" data-form-target="customForm">
  <button name="custom_submit" type="button"    data-action="click->form#custom_submit">送信
  </button>
</form>
  1. form要素にコントローラー名を記載(data-controller=”form”)して、フォームとstimulusコントローラーを接続。
  2. カスタムロジックの処理が成功したときにフォームをコントローラーで送信できるように、フォーム要素をコントローラーのターゲットとして指定(data-form-target=”customForm”)
  3. ボタンクリック時にコントローラーのアクションが実行されるように、クリックイベントハンドラーを記載(data-action=”click->form#custom_submit”)。

これで、type属性がbuttonであっても、ボタンをクリックすると重複送信防止のためにボタンが無効化され、再送信の必要があるときは有効化されるという、Rails UJSと同様の効果を得ることができます。

まとめ

Rails UJSは、フォームの二重送信を防止するための便利なツールですが、特定の状況では使えない場合があります。UJSが使えない場合は、stimulus.jsなどの代替手段を用いることで、二重送信を確実に防止することができます。

参考情報

Rails UJSレポジトリー