Railsではリンクやフォームに簡単に確認ダイアログを出すための記法があります。aタグやinputタグなどにdata-confirm="よろしいですか?"みたいにするやつですね。

1<%= link_to 'delete', item_path(item), data: { confirm: 'are you sure?' } %>

これをするとブラウザの確認ダイアログが出ます。ここでキャンセルした場合はリクエストは投げられません。

というものなのですが、これをかっこよくしたいとか「以後表示しない」みたいなことをさせたくないみたいな理由で、ブラウザのダイアログを使いたくないみたいなことが場合によっては発生します。

↑のような機能はrails-ujsというjavascriptによって提供されています(他の処理もいろいろやっています)。元々はjquery-ujsという名前でしたが、jQueryへの依存をなくすような形で書き換えられ、確かRails 5.1から標準に取り込まれました。

jquery-ujsの頃は↓のような方法で自前ダイアログに変更できたのですが、rails-ujsになってからはこの方法は使えません。

rails-ujsを使用している場合はaタグのクリックイベントを単純に乗っ取るだけでは自前実装のダイアログに置き換えることはできません。また、data-confirm以外にもrails-ujsが色々やっている(たとえばremote: trueなリンクの処理など)ため、単純に乗っ取ってしまうと想定した機能が動かなくなることもあります。

そういうわけで必要な箇所だけを正しく乗っ取ってあげる必要があります。幸いコード例を見つけたのでそちらを参考に書いてみましょう。

こちらの場合はsweet-alertというJSライブラリを使ってダイアログを表示しています。またujsのdata-confirmを上書きしないよう独自のdata-confirm-swalという属性を使うふうにかいていますが、data-confirm自体を乗っ取ってしまっても問題なく動きます。

まず、自前のイベントハンドラとクリックイベントへの紐づけの部分がこんな感じになります(元のコードの一部(data-confirm-swalの変更、対象とする要素のセレクタinput\[data-confirm\]のあたり)を修正)。

 1(function() {
 2  var handleConfirm = function(element) {
 3    if (!allowAction(this)) {
 4      Rails.stopEverything(element)
 5    }
 6  }
 7
 8  var allowAction = function(element) {
 9    if (element.getAttribute('data-confirm') === null) {
10      return true
11    }
12
13    showConfirmationDialog(element)
14    return false
15  }
16
17  document.addEventListener('rails:attachBindings', function(e) {
18    Rails.delegate(document, 'a[data-confirm], input[data-confirm], button[data-confirm]', 'click', handleConfirm)
19  })
20}).call(this)

:deleteメソッドなリンクが正しく動くようにとか、Rails.start後にこちらのやりたい処理を差し込めるように、といった工夫のためrails:attachbindingsのリスナーでイベントハンドラの登録を行います。

クリック後、allowActionでconfirmを出すべき属性があるかないかを判断し、出すべきであればshowConfirmationDialogを呼び出すという流れです。

その後は自分で出したいダイアログを表示するわけですが、1つポイントがあります。元記事(一部data-confirm-swalを修正)の例を見てみます。

 1  // Display the confirmation dialog
 2  var showConfirmationDialog = function(element) {
 3    var message = element.getAttribute('data-confirm')
 4    var text = element.getAttribute('data-text')
 5
 6    swal({
 7      title: message || 'Are you sure?',
 8      text: text || '',
 9      type: 'warning',
10      showCancelButton: true,
11      confirmButtonText: 'Yes',
12      cancelButtonText: 'Cancel',
13    }).then(function(result) { // <= OKを押した際のコールバック
14      confirmed(element, result)
15    })
16  }
17
18  var confirmed = function(element, result) {
19    if (result.value) {
20      // User clicked confirm button
21      element.removeAttribute('data-confirm')
22      element.click()
23    }
24  }

JSライブラリによってダイアログを表示し、OKが押された場合にはdata-confirmを消した上でJSからクリックします。すると今度は確認ダイアログなしでクリックされたことになり、元々想定していた通りのリクエストが送られる、というわけです。

ダイアログの実装自体はなんでもよくて、例えばjquery-confirmのようなものでもOKです。

元記事への補足として、remote: trueなリクエストの場合、画面全体が更新されないことが多いかと思います。場合によってはdata-confirmを削除した状態のリンクがそのまま残っている場合もあり得ます。

この場合、該当のリンクをクリックすると今度は確認なしでリクエストを投げてしまいますのであまりよくありません。ということで元のダイアログの部分のコードを修正し、たとえばダイアログが閉じたらdata-confirmを戻しておくなどの工夫が必要です。

1element.setAttribute('data-confirm', message)