Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsフロントエンド: HotwireとTailwind CSSでモーダルを作る(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Railsフロントエンド: HotwireとTailwind CSSでモーダルを作る(翻訳)

2021年に導入されたHotwireのおかげで、Rails開発者はJavaScriptをほとんど書かずに高度なUIを作成できるようになりました。

モーダルは、近年のWebアプリで最も多用されるUIの1つです。たしかにHotwireならモーダルを非常に作成できますが、気をつけておきたい注意点がいくつかあります。モーダルに関するハウツー記事は既にいろいろありますが、本記事ではより多くの(新しい)アイデアも見ていきたいと思います。

モーダル(ダイアログとも呼ばれます)は、本質的にアプリの他の部分の上にトッピングされるコンポーネントです(なお、より複雑な処理が可能なdialog要素もあります)。

つまり、RailsとHotwireでモーダルを作るのに必要なのは、以下の2つです。

  1. Turbo Frame(モーダルを読み込む)
  2. ラッパー要素(モーダルの実際のコンテンツをラップする)

🔗 モーダルの基本

アプリケーションのレイアウトテンプレートにTurbo Frameを追加します。

<!-- app/views/layouts/application.html.erb -->
<%= turbo_frame_tag "modal" %>

次に、モーダルとして使いたいビューを以下のようにラップします。

<!-- app/views/users/new.html.erb -->

<turbo-frame id="modal">
  <!-- モーダルのラッパー -->
  <div role="dialog" tabindex="-1" class="fixed inset-0 z-30 flex items-center justify-center w-full h-screen p-2">

    <!-- 背景 -->
    <div class="fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30"></div>

    <div class="relative z-20 w-full max-w-2xl max-h-screen overflow-y-auto bg-white shadow-lg rounded-md">
      <!-- モーダルのコンテンツをここに置く(`form_with`ヘルパーなど)-->
    </div>
  </div>
</turbo-frame>

次に、以下のリンクでモーダルを表示します。

<!-- app/views/users/index.html.erb-->

<%= link_to "Create new user", new_user_path, data: {turbo_frame: "modal"} %>

これで、上のリンクをクリックすると、app/views/users/new.html.erbturbo-frameタグ間にあるコンテンツが、app/views/layouts/application.html.erbレイアウトに追加されているturbo_frame_tag "modal"内でレンダリングされます。また、追加されているTailwind CSSによって、コンテンツはアプリの「最前面」に表示されます。

以上が、RailsでHotwireの機能を用いてモーダルを表示するときの基本です。

✨ヒント

StimulusJSを用いるさらに進んだモーダルを手軽に使う方法については、Rails Designer特製のモーダルをぜひチェックしてみてください。このモーダルは、ViewComponentで構築され、Tailwind CSSでデザインされ、Hotwireで強化されています。

🔗 基本の先に進む

さて、基本のモーダルは楽勝でしたが、このモーダルを開いた後で閉じる方法がないのは良くありません。このあたりはシンプルな方法で改善できます。

最初に、背景用のdivを以下のようにbutton_toに変更してみましょう。

# app/views/users/new.html.erb
-<div class="fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30"></div>
+<%= button_to nil, nil, type: :button, method: :get, form: { data: {action: "modal#hide"} }, class: "fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30" %>

このモーダルをシンプルなStimulusコントローラで起動してみましょう。

🔗 モーダルをStimulusで拡張する

訳注: bin/rails g stimulus modalでStimulusコントローラを追加してから以下に置き換えるとよいでしょう。

// app/javascript/controllers/modal_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  connect() {
    this.element.focus();
  }

  hide(event) {
    event.preventDefault();

    this.element.remove();
  }

  hideOnSubmit(event) {
    if (event.detail.success) {
      this.hide();
    }
  }

  disconnect() {
    this.#modalTurboFrame.src = null;
  }

  // private

  get #modalTurboFrame() {
    return document.querySelector("turbo-frame[id='modal']");
  }
}

次に、app/views/users/new.html.erbを以下のように更新します。

<turbo-frame id="modal">
  # ...
- <div role="dialog" tabindex="-1" class="fixed inset-0 z-30 flex items-center justify-center w-full h-screen p-2">
+ <div data-controller="modal" data-action="turbo:submit-end->modal#hideOnSubmit keydown.esc->modal#hide" role="dialog" tabindex="-1" class="fixed inset-0 z-30 flex items-center justify-center w-full h-screen p-2">
    # ...
  </div>
</turbo-frame>

このStimulusコントローラが実際に行っているのは、turbo-framesrc属性を以下の場合にリセットすることだけです。

  • 背景をクリックしたとき
  • フォーム送信が成功したとき
  • Escキーを押したとき

続いて、connect()の要素にフォーカスを移動します。これが可能なのは、その要素のtabindex属性が負の値になっているためです。

必要であれば、new.html.erbに以下を追加することでモーダル内に「キャンセル」ボタンも作成できます(キャンセルボタンは多くの場合「確認」ボタンや「保存」ボタンの隣に配置されます)。

 <%= button_tag "Cancel", type: :button, method: :get, data: {action: "modal#hide"}, class: "px-3 py-1 text-sm leading-6 font-medium text-gray-700 bg-white border border-gray-200 rounded-md hover:border-gray-300" %>

ここで、Railsの通常のbutton_tagヘルパーに加えて、data-action属性にmodal#hideという値も設定していることにご注目ください。小さく再利用可能なJavaScriptコードをこのような形でHTMLに対して利用できるのがStimulusの優れた点です。

🔗 ビューをモーダル専用にする

もうひとつ追加したい便利技は、モーダルのビューがモーダルとしてのみ表示されるようにする(users/new.html.erbなどのように単独ページとして表示されることがないようにする)方法です。これは、以下の過去記事でも取り上げています。

Railsフロントエンド: ViewComponent+Tailwind CSS+Hotwireの便利技8つ(翻訳)

これは以下のように、Railsコントローラ用の小さなconcernとして利用します。

# app/controllers/concerns/frameable.rb

module Frameable
  extend ActiveSupport::Concern

  private

  def ensure_turbo_frame_response
    redirect_to root_path unless turbo_frame_request?
  end

  def production_environment?
    Rails.env.production?
  end
end

続いて、UsersControllerに以下を追加します。

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :ensure_turbo_frame_response, only: %w[new], if: :production_environment?
end

これで、アプリのユーザーはusers/newビューをTurbo Frame経由で(つまりモーダルとして)しか表示できないようになります。ただしこの振る舞いはproduction環境でのみ有効です(if: :production_environment?)。ビューの設計段階では、モーダルよりも通常の単独ページとして表示する方が手っ取り早いので、このフラグを追加しておくのが私の好みです。

Railsアプリでモーダルを利用する基本テクニックは、以上でおしまいです。お気づきの点や他のアイデアがありましたら、メールでお気軽にお知らせください。

関連記事

Rails: ビューやコンポーネントに条件付きでCSSクラスを追加する(翻訳)

HTMLDialogElement: showModal() はHTMLの長年の課題を解決する期待の星かと思ったら実際はだめな子


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。