data-turbo-confirm の確認ダイアログをカスタマイズする方法

## 完成イメージ

![turbo-confirm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/990603/c42d3330-8c6c-fa43-40ba-08513f723509.gif)

## はじめに

Turbo では、フォームやリンクに `data-turbo-confirm` 属性をつけると確認ダイアログを表示できます。

https://turbo.hotwired.dev/handbook/drive#requiring-confirmation-for-a-visit

https://railsguides.jp/working_with_javascript_in_rails.html#%E7%A2%BA%E8%AA%8D%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0

通常はブラウザ標準の確認ダイアログ `window.confirm()` が使われます。

https://developer.mozilla.org/ja/docs/Web/API/Window/confirm

https://github.com/hotwired/turbo/blob/2b41a936163eaa3bfc2e7b3bb05dbe7ef803303b/src/core/drive/form_submission.ts#L59-L65

実は、`Turbo.setConfirmMethod()` で `window.confirm()` ではなく別の処理を実行するように設定できます。

https://turbo.hotwired.dev/reference/drive#turbo.setconfirmmethod

https://github.com/hotwired/turbo/blob/1ec72ac3236038af50a0010f0821e0664eeac087/src/core/index.ts#L126-L130

適当な関数を `Turbo.setConfirmMethod()` の引数にすることで、`window.confirm()` の代わりに独自の確認ダイアログを表示できるようになります。
引数にする関数が `Promise<boolean>` を返すのであれば、[dialog](https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog) タグを使うことも、[Bootstrap](https://getbootstrap.com/) のような CSSフレームワークのモーダルを使うことも、[Micromodal.js](https://micromodal.vercel.app/) のようなライブラリを使うこともできます。

:::note warn
Turbo のバージョンは 7.2.0 以上である必要があります。
非同期な処理が必要なためです。
https://github.com/hotwired/turbo/releases/tag/v7.2.0
https://github.com/hotwired/turbo/pull/525
:::

この記事では具体例として、Bootstrap の[モーダル](https://getbootstrap.com/docs/5.2/components/modal/)を確認ダイアログとして使う方法を紹介します。

## カスタマイズ例（ Bootstrap のモーダルを使う場合）

### 環境

| フレームワーク、ライブラリなど                 | バージョン |
| ---------------------------------------------- | ---------- |
| [Rails](https://github.com/rails/rails)        | 7.0.3.1    |
| [Turbo](https://github.com/hotwired/turbo)     | 7.2.2      |
| [Bootstrap](https://github.com/twbs/bootstrap) | 5.2.0      |

### フォームに `data-turbo-confirm` 属性

確認ダイアログを出したいフォームやリンクに `data-turbo-confirm` 属性を追記します。

```erb
<%= button_to '削除', post_path(@post), method: :delete, class: 'btn btn-outline-danger', form: { data: { turbo_confirm: '本当によろしいですか？' } } %>
```

### レイアウトファイルにモーダルの HTML

app/views/layouts/application.html.erb にモーダルの HTML を追記します。
モーダルの要素、メッセージの要素、承認ボタンに id 属性をつけています。
JavaScript で読み取るためです。

```erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <!-- ... -->
  <body>
    <%= yield %>

    <div class="modal fade" id="turbo-confirm-modal">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-body" id="turbo-confirm-modal-message"></div>
          <div class="modal-footer">
            <button class="btn btn-light" data-bs-dismiss="modal">キャンセル</button>
            <button class="btn btn-primary" id="turbo-confirm-modal-confirm-button">OK</button>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>
```

### モーダルを表示する関数を定義し、`Turbo.setConfirmMethod()` の引数に

app/javascript/lib/bootstrap_confirm.js では、`bootstrapConfirm()` を定義し、`Turbo.setConfirmMethod()` の引数にしています。
lib/bootstrap_confirm.js というファイル名と `bootstrapConfirm` という関数名は好きに変えても問題ないです。

`bootstrapConfirm()` は、

- モーダルのメッセージを変更
- モーダルを表示
- 承認ボタンがクリックされたときに `Promise.resolve(true)` を返す

ということを行っています。

`message` には Turbo が `data-turbo-confirm` の値を代入してくれます。

```javascript:app/javascript/lib/bootstrap_confirm.js
import { Modal } from 'bootstrap'

const bootstrapConfirm = (message) => {
  const modalElement = document.getElementById('turbo-confirm-modal')
  const messageElement = document.getElementById('turbo-confirm-modal-message')
  const confirmButtonElement = document.getElementById('turbo-confirm-modal-confirm-button')
  const modal = new Modal(modalElement)

  messageElement.textContent = message
  modal.show()

  return new Promise((resolve) => {
    confirmButtonElement.addEventListener(
      'click',
      () => {
        resolve(true)
        modal.hide()
      },
      { once: true },
    )
  })
}

Turbo.setConfirmMethod(bootstrapConfirm)
```

### エントリーポイントで読み込む

エントリーポイントの app/javascript/application.js に `import './lib/bootstrap_confirm'` を追記します。

```javascript:app/javascript/application.js
// Entry point for the build script in your package.json
import '@hotwired/turbo-rails'
import './controllers'
import 'bootstrap'
import './lib/bootstrap_confirm'
```

これで `data-turbo-confirm` の確認ダイアログで Bootstrap のモーダルを使うことができます。

![turbo-confirm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/990603/c42d3330-8c6c-fa43-40ba-08513f723509.gif)

### さらにカスタマイズ

`bootstrapConfirm()` の引数には最大で3つまで渡すことができます。Turbo によって、順に

- `data-turbo-confirm` の値
- `data-turbo-confirm` がついているフォーム要素
- `data-turbo-confirm` がついているフォーム要素のサブミットボタン

が代入されます。

https://github.com/hotwired/turbo/blob/2b41a936163eaa3bfc2e7b3bb05dbe7ef803303b/src/core/drive/form_submission.ts#L129

例えば、コードを

```erb
<%= button_to '削除', post_path(@post), method: :delete, class: 'btn btn-outline-danger', form: { data: { turbo_confirm: '本当によろしいですか？' } } %>
```

```javascript:app/javascript/lib/bootstrap_confirm.js
const bootstrapConfirm = (message, formElement, submitter) => {
  console.log(message)
  console.log(formElement)
  console.log(submitter)
}

Turbo.setConfirmMethod(bootstrapConfirm)
```

とすれば、`message`, `formElement`, `submitter` は

```html
<!-- console.log(message) -->
本当によろしいですか？

<!-- console.log(formElement) -->
<form data-turbo-confirm="本当によろしいですか？" class="button_to" method="post" action="/posts/1">
  <input type="hidden" name="_method" value="delete" autocomplete="off">
  <button class="btn btn-outline-danger" type="submit">削除</button>
  <input type="hidden" name="authenticity_token" value="Zhb-6EgmOLl9puHT" autocomplete="off">
</form>

<!-- console.log(submitter) -->
<button class="btn btn-outline-danger" type="submit">削除</button>
```

となります。

このことを利用すると、`formElement` から取得できるフォームに `data-ooo` をつけて、モーダルをフォームによって少し変化させることもできると思います。

```erb
<%= button_to '更新', post_path(@post), class: 'btn btn-primary', form: { data: { turbo_confirm: '本当に更新しますか？', turbo_confirm_title: '投稿の編集', turbo_confirm_button_class_list: 'btn btn-primary' } } %>

<%= button_to '削除', post_path(@post), method: :delete, class: 'btn btn-outline-danger', form: { data: { turbo_confirm: '本当に削除しますか？', turbo_confirm_title: '投稿の削除', turbo_confirm_button_class_list: 'btn btn-danger' } } %>
```

```javascript:app/javascript/lib/bootstrap_confirm.js
import { Modal } from 'bootstrap'

const bootstrapConfirm = (message, formElement, submitter) => {
  // ...
  const titleElement = document.getElementById('turbo-confirm-modal-title')
  const messageElement = document.getElementById('turbo-confirm-modal-message')
  const confirmButtonElement = document.getElementById('turbo-confirm-modal-confirm-button')
  // ...

  titleElement.textContent = formElement.getAttribute('data-turbo-confirm-title')
  messageElement.textContent = message
  confirmButtonElement.classList = formElement.getAttribute('data-turbo-confirm-button-class-list')
  confirmButtonElement.textContent = submitter.textContent
  modal.show()

  // ...
}

Turbo.setConfirmMethod(bootstrapConfirm)
```

とすると、モーダルのタイトルとボタンのテキストと色をフォームによって変更できます。

![turbo-confirm-dynamic.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/990603/f19c005f-3e85-471b-ed8d-3f5e7bdd1f86.gif)

## 参考リンク

### GoRails

dialog タグを使って、確認ダイアログをカスタマイズする方法が紹介されています。

<iframe width="560" height="315" src="https://www.youtube.com/embed/jn22KyMEdwg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

https://gorails.com/episodes/custom-hotwire-turbo-confirm-modals?autoplay=1

### 動画中のコード

https://github.com/gorails-screencasts/custom-turbo-confirm-modal/blob/8933ca34c1c047a83ea3c10410bf920b8cc70bf4/app/views/posts/show.html.erb#L9

https://github.com/gorails-screencasts/custom-turbo-confirm-modal/blob/master/app/views/layouts/application.html.erb#L16-L24

https://github.com/gorails-screencasts/custom-turbo-confirm-modal/blob/master/app/javascript/application.js#L5-L16