🦓

[Rails]tailwindcss-stimulus-componentsによるモーダル

2023/08/03に公開

はじめに

投稿の削除を行う前に、削除してよろしいでしょうか、という確認メッセージを含むモーダルウィンドウを見たことがありますね。
重要なデータや情報を誤って削除することを防止するためによく使われます。
ユーザーが誤って削除しないように確認を促す機能ですね。

モーダルウィンドウとは、通常、ユーザーが何か特定のアクションを実行する前に情報を確認したり、選択を行ったりする際に使用されます。
モーダルウィンドウは他のコンテンツとのインタラクションを一時的に制限し、重要な情報やタスクに焦点を当てることができます。

tailwindcss-stimulus-componentsを使って開発中のRailsアプリにモーダルウィンドウを入れていきます。
https://github.com/excid3/tailwindcss-stimulus-components

環境

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

コンポーネントを読み込む

app/javascript/controllers/index.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)

https://github.com/excid3/tailwindcss-stimulus-components#basic-usage

データ属性を設定する

バックグラウンドクリックをtrueにします。
ボタンにクリックイベント→modalコントローラー→open関数を発火させます。
_destroy_confirmation.html.erbを読みこみます。

app/views/ideas/_idea.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関数を発火させます。

app/views/shared/_destroy_confirmation.html.erb
<!-- 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コントローラーを定義する

openclosecloseBackgroundのアクションが必要ですね。
スクロールポジションに関してのアクションも用意してくれました。

app/javascript/controllers/modal_controller.js
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 targetsstatic values プロパティ:

  • targets プロパティは、HTML要素のセレクタを指定して、その要素をコントローラーのインスタンスのプロパティとして利用できるようにします。
  • values プロパティは、コントローラーのインスタンスの値として利用できるプロパティを定義します。ここでは、backdropColorrestoreScroll の2つの値が定義されています。

connect メソッド:

  • コントローラーがDOMに接続されたときに呼び出されるメソッドです。このメソッドでは、コントローラーのプロパティを初期化しています。

open メソッド:

  • モーダルウィンドウを開くときに呼び出されるメソッドです。引数 e はイベントオブジェクトです。
  • e.preventDefault() は、デフォルトのイベントアクションをキャンセルします。
  • this.lockScroll() は、スクロールをロックし、現在のスクロール位置を保存します。
  • this.containerTarget は、static targets で定義された container 要素を指します。モーダルウィンドウの表示・非表示を切り替えるための要素です。
  • this.background は、モーダルウィンドウの背後に表示される背景要素を指します。

close メソッド:

  • モーダルウィンドウを閉じるときに呼び出されるメソッドです。引数 e はイベントオブジェクトです。
  • e.preventDefault() は、デフォルトのイベントアクションをキャンセルします。
  • this.unlockScroll() は、スクロールをアンロックし、保存されたスクロール位置を復元します。

closeBackground メソッド:

  • 背景をクリックしたときに呼び出されるメソッドです。allowBackgroundClosetrue の場合、モーダルウィンドウを閉じるために 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() で、fixedinset-x-0overflow-hidden の3つのクラスを<body>要素に追加して、スクロールをロックします。
  • this.restoreScrollValuetrue の場合、現在のスクロール位置を保存してモーダルウィンドウを開いた時点での位置を維持します。

unlockScroll() 関数:

  • スクロールをアンロックするための処理を行います。
  • document.body.style.paddingRightnull に設定し、スクロールバーの幅に関連する余白を削除します。
  • document.body.classList.remove() で、fixedinset-x-0overflow-hidden の3つのクラスを<body>要素から削除して、スクロールをアンロックします。
  • this.restoreScrollValuetrue の場合、保存されたスクロール位置を復元します。

saveScrollPosition() 関数:

  • 現在のページの垂直スクロール位置を保存します。
  • window.pageYOffset または document.body.scrollTop を使用して、スクロール位置を取得し、this.scrollPosition に保存します。

restoreScrollPosition() 関数:

  • スクロール位置を復元するための処理を行います。
  • this.scrollPosition に保存されているスクロール位置を使って、ページの垂直スクロール位置を復元します。

これらの関数を使って、モーダルウィンドウが開閉される際にスクロールのロックとアンロックが行われ、ページのスクロール位置が維持されるようになっています。

ソースコードを読みながら理解していきましょう。
https://github.com/excid3/tailwindcss-stimulus-components/blob/master/src/modal.js

ブラウザのデフォルトのダイアログより見やすくなりましたね。
Image from Gyazo

終わりに

tailwindcss-stimulus-componentsが便利ですね。

Discussion