[Rails]tailwindcss-stimulus-componentsによるモーダル
はじめに
投稿の削除を行う前に、削除してよろしいでしょうか、という確認メッセージを含むモーダルウィンドウを見たことがありますね。
重要なデータや情報を誤って削除することを防止するためによく使われます。
ユーザーが誤って削除しないように確認を促す機能ですね。
モーダルウィンドウとは、通常、ユーザーが何か特定のアクションを実行する前に情報を確認したり、選択を行ったりする際に使用されます。
モーダルウィンドウは他のコンテンツとのインタラクションを一時的に制限し、重要な情報やタスクに焦点を当てることができます。
tailwindcss-stimulus-components
を使って開発中のRailsアプリにモーダルウィンドウを入れていきます。
環境
Rails 7.0.4.3
ruby 3.2.1
tailwindcss-stimulus-components
をインストールする
bin/importmap pin tailwindcss-stimulus-components
Pinning "tailwindcss-stimulus-components" to https://ga.jspm.io/npm:tailwindcss-stimulus-components@3.0.4/dist/tailwindcss-stimulus-components.modern.js
Pinning "@hotwired/stimulus" to https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js
stimulusコントローラーを作成する
bin/rails g stimulus modal
create app/javascript/controllers/modal_controller.js
コンポーネントを読み込む
// Start StimulusJS
import { Application } from "@hotwired/stimulus"
const application = Application.start();
// Import and register all TailwindCSS Components
+ import { Modal } from "tailwindcss-stimulus-components"
+ application.register('modal', Modal)
データ属性を設定する
バックグラウンドクリックをtrue
にします。
ボタンにクリックイベント→modal
コントローラー→open
関数を発火させます。
_destroy_confirmation.html.erb
を読みこみます。
<div data-controller="modal" data-modal-allow-background-close="true" class="my-20">
<button data-action="click->modal#open" class="inline-flex items-center rounded-md bg-red-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" />
</svg>
<span class="ml-2">削除</span>
</button>
<%= render 'ideas/destroy_confirmation', item: idea %>
</div>
_destroy_confirmation.html.erb
を作成する
モーダルウィンドウ用のパーシャルファイルを作成します。
アクションにマウスクリックとキーボード操作二つありましたがクリックだけにします。
モーダルが開いている状態→クリックイベント→modal
コントローラー→closeBackground
関数を発火させます。
<!-- Modal Background -->
<div class="hidden fixed inset-0 bg-black bg-opacity-80 overflow-y-auto flex items-center justify-center"
data-modal-target="background"
data-action="click->modal#closeBackground">
<!-- Modal Container -->
<div data-modal-target="container"
class="hidden animated fadeIn fixed inset-0 overflow-y-auto flex items-center justify-center">
<!-- Modal Inner Container -->
<div class="max-h-screen w-full max-w-lg relative">
<!-- Modal Card -->
<div class="m-1 bg-white rounded shadow">
<div class="p-8">
<h2 class="text-xl mb-4">削除しますか?</h2>
<p class="mb-4">削除した投稿を戻すことができません。</p>
<div class="flex justify-end items-center flex-wrap mt-6">
<%= link_to "キャンセル", item, data: { action: "click->modal#close" }, class: "mr-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
<%= link_to "削除", item, data: {
turbo_method: :delete }, class: "bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" %>
</div>
</div>
</div>
</div>
</div>
</div>
modal
コントローラーを定義する
open
、close
、closeBackground
のアクションが必要ですね。
スクロールポジションに関してのアクションも用意してくれました。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ['container']
static values = {
backdropColor: { type: String, default: 'rgba(0, 0, 0, 0.8)' },
restoreScroll: { type: Boolean, default: true }
}
connect() {
// モーダルウィンドウの表示の切り替えに使用されるクラス名
this.toggleClass = this.data.get('class') || 'hidden';
// モーダルウィンドウの背後に配置される背景要素のID属性を指定する
this.backgroundId = this.data.get('backgroundId') || 'modal-background';
// 背景要素のHTML
this.backgroundHtml = this.data.get('backgroundHtml') || this._backgroundHTML();
// 背景をクリックすることでモーダルウィンドウを閉じることができるかどうか
this.allowBackgroundClose = (this.data.get('allowBackgroundClose') || 'true') === 'true';
// モーダルウィンドウが開かれる際に、クリックされた要素のデフォルトアクション(例:リンクの遷移など)をキャンセルする
this.preventDefaultActionOpening = (this.data.get('preventDefaultActionOpening') || 'true') === 'true';
// モーダルウィンドウが閉じられる際に、クリックされた要素のデフォルトアクション(例:リンクの遷移など)をキャンセルする
this.preventDefaultActionClosing = (this.data.get('preventDefaultActionClosing') || 'true') === 'true';
}
...
static targets
と static values
プロパティ:
-
targets
プロパティは、HTML要素のセレクタを指定して、その要素をコントローラーのインスタンスのプロパティとして利用できるようにします。 -
values
プロパティは、コントローラーのインスタンスの値として利用できるプロパティを定義します。ここでは、backdropColor
とrestoreScroll
の2つの値が定義されています。
connect
メソッド:
- コントローラーがDOMに接続されたときに呼び出されるメソッドです。このメソッドでは、コントローラーのプロパティを初期化しています。
open
メソッド:
- モーダルウィンドウを開くときに呼び出されるメソッドです。引数
e
はイベントオブジェクトです。 -
e.preventDefault()
は、デフォルトのイベントアクションをキャンセルします。 -
this.lockScroll()
は、スクロールをロックし、現在のスクロール位置を保存します。 -
this.containerTarget
は、static targets
で定義されたcontainer
要素を指します。モーダルウィンドウの表示・非表示を切り替えるための要素です。 -
this.background
は、モーダルウィンドウの背後に表示される背景要素を指します。
close
メソッド:
- モーダルウィンドウを閉じるときに呼び出されるメソッドです。引数
e
はイベントオブジェクトです。 -
e.preventDefault()
は、デフォルトのイベントアクションをキャンセルします。 -
this.unlockScroll()
は、スクロールをアンロックし、保存されたスクロール位置を復元します。
closeBackground
メソッド:
- 背景をクリックしたときに呼び出されるメソッドです。
allowBackgroundClose
がtrue
の場合、モーダルウィンドウを閉じるためにthis.close()
を呼び出します。
closeWithKeyboard
メソッド:
- キーボードの
ESC
キーを押したときに呼び出されるメソッドです。モーダルウィンドウが開いている場合に限り、this.close()
を呼び出します。
closeWithKeyboard
メソッド以後のコードは、モーダルウィンドウが開かれたときにスクロールをロックし、モーダルウィンドウが閉じられたときにスクロールをアンロックするための関数群です。
_backgroundHTML()
関数:
- モーダルウィンドウの背後に表示される背景要素を生成します。
-
this.backgroundId
は、背景要素のID属性を指定します。 -
this.backdropColorValue
は、背景要素の背景色を指定します。 - 生成される背景要素は、指定されたID属性と背景色を持ち、画面全体を覆うように設定されます。
lockScroll()
関数:
- スクロールをロックするための処理を行います。
-
window.innerWidth - document.documentElement.clientWidth
で、スクロールバーの幅を計算しています。これは、スクロールバーが表示されている場合にページの横幅が変わることを防ぐためです。 -
document.body.style.paddingRight
にスクロールバーの幅を設定することで、スクロールバーの幅分だけ右側に余白が生じることを防ぎます。 -
document.body.classList.add()
で、fixed
、inset-x-0
、overflow-hidden
の3つのクラスを<body>
要素に追加して、スクロールをロックします。 -
this.restoreScrollValue
がtrue
の場合、現在のスクロール位置を保存してモーダルウィンドウを開いた時点での位置を維持します。
unlockScroll()
関数:
- スクロールをアンロックするための処理を行います。
-
document.body.style.paddingRight
をnull
に設定し、スクロールバーの幅に関連する余白を削除します。 -
document.body.classList.remove()
で、fixed
、inset-x-0
、overflow-hidden
の3つのクラスを<body>
要素から削除して、スクロールをアンロックします。 -
this.restoreScrollValue
がtrue
の場合、保存されたスクロール位置を復元します。
saveScrollPosition()
関数:
- 現在のページの垂直スクロール位置を保存します。
-
window.pageYOffset
またはdocument.body.scrollTop
を使用して、スクロール位置を取得し、this.scrollPosition
に保存します。
restoreScrollPosition()
関数:
- スクロール位置を復元するための処理を行います。
-
this.scrollPosition
に保存されているスクロール位置を使って、ページの垂直スクロール位置を復元します。
これらの関数を使って、モーダルウィンドウが開閉される際にスクロールのロックとアンロックが行われ、ページのスクロール位置が維持されるようになっています。
ソースコードを読みながら理解していきましょう。
終わりに
tailwindcss-stimulus-components
が便利ですね。
Discussion