Railsフロントエンド: HotwireとTailwind CSSでモーダルを作る(翻訳)
2021年に導入されたHotwireのおかげで、Rails開発者はJavaScriptをほとんど書かずに高度なUIを作成できるようになりました。
モーダルは、近年のWebアプリで最も多用されるUIの1つです。たしかにHotwireならモーダルを非常に作成できますが、気をつけておきたい注意点がいくつかあります。モーダルに関するハウツー記事は既にいろいろありますが、本記事ではより多くの(新しい)アイデアも見ていきたいと思います。
モーダル(ダイアログとも呼ばれます)は、本質的にアプリの他の部分の上にトッピングされるコンポーネントです(なお、より複雑な処理が可能なdialog
要素もあります)。
つまり、RailsとHotwireでモーダルを作るのに必要なのは、以下の2つです。
- Turbo Frame(モーダルを読み込む)
- ラッパー要素(モーダルの実際のコンテンツをラップする)
🔗 モーダルの基本
アプリケーションのレイアウトテンプレートに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.erb
のturbo-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-frame
のsrc
属性を以下の場合にリセットすることだけです。
- 背景をクリックしたとき
- フォーム送信が成功したとき
- 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コントローラ用の小さな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アプリでモーダルを利用する基本テクニックは、以上でおしまいです。お気づきの点や他のアイデアがありましたら、メールでお気軽にお知らせください。
関連記事
HTMLDialogElement: showModal() はHTMLの長年の課題を解決する期待の星かと思ったら実際はだめな子
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。