Rails: ViewComponentとTailwind CSSやHotwireを効果的に組み合わせる(翻訳)
フロントエンドのコードは、歴史的にどことなく下に見られがちでした。「HTMLは本物の言語じゃない!」「CSS最低!」「JavaScriptなんか見たくもない!」といった具合です。せっかくRailsが、1人による開発チームに真の力、すなわちゼロから完全な製品を構築し、顧客を獲得して成功を収められるパワーを与えてくれるというのに、これは嘆かわしいことです。
Rails本体や周辺には、Railsを書く楽しさをさらに高めてくれるツールが他にも控えています。それがすなわちHotwireであり、Tailwind CSSであり、ViewComponentです。
私はこれまで、RailsによるSaaSアプリを構築するときにそうしたツールを効果的に活用するtipsや秘訣を(そしてベストプラクティスをも)積み上げてきました。書き漏らしたものがあれば今後本記事を更新することもあると思います。
🔗 秘訣1: CSSクラスのリストを(条件付きで)構築するにはRailsのclass_names()を使おう
Railsのclass_names()ヘルパーを使うと以下のようなアプローチをさくっと利用できるので、私はいつも重宝しています。
- CSSクラスを条件に応じて追加または削除する
- Tailwind CSSの長くなりがちなユーティリティクラスをきれいに管理できるようにする
RailsのAPIドキュメントにも書かれているように、class_names()ヘルパーはtoken_list()タグヘルパーの単なるエイリアスです。
実例を見てみましょう(Rails DesignerのDropdownCompoentより)。
class_names(
"absolute text-sm shadow-xl overflow-hidden rounded-lg z-10",
content_min_max_width,
@padding,
{
"bg-white/80 ring-1 ring-inset ring-gray-100 backdrop-blur-md": @theme.light?,
"bg-gray-800": @theme.light?
}
)
- 1行目にある
absoluteからz-10までは、Tailwind CSSの「静的な」ユーティリティクラスで、これらは常に適用されます。 - その下の
content_min_max_widthというメソッドでは、CSSクラスをいくつか設定しています(ここで何らかの追加処理を行っていることは何となく想像がつくでしょう)。 - その下の
@paddingインスタンス変数で何かが追加されています(これはコンポーネントに追加される属性であることがわかりますね)。 - 次の波かっこ
{}の中にあるクラスは、@theme.light?がtrueの場合に設定されるので、明るいテーマなら白、暗いテーマならグレーになります。
このとき、DropdownComponentでビルドされるデフォルトのクラスは以下のリストになります。
"absolute text-sm shadow-xl overflow-hidden rounded-lg z-10 min-w-[8rem] max-w-[16rem] p-4 bg-white/80 ring-1 ring-inset ring-gray-100 backdrop-blur-md"
このclass_names()はtagヘルパーメソッドにも「組み込まれている」ので、tagヘルパーメソッドのclass引数でも以下のようにCSSクラスを条件付きで書けます。
tag("div", class: { "block": Current.user.admin?, "hidden": !Current.user.admin? })
# => <div class="block" /> # 管理者の場合
# => <div class="hidden" /> # それ以外の場合
🔗 秘訣2: link_toの<a>タグにデフォルトのスタイルを与える方法
CSSも長い年月を経て随分強力になってきたので、昔のようにCSSをクラス(.class)やid(#unique_id)でちまちま指定する時代はもう終わりました。現代なら、HTML要素を選択するときに他の属性も利用できます。
私はTailwind CSSの@layer baseで、以下のような@applyディレクティブを常用しています。
@layer base {
/* ... */
a:not([class]) {
@apply underline;
&:hover {
@apply no-underline;
}
}
/* 省略 */
}
これで、Railsのlink_toヘルパーによるリンクに下線がグローバルに追加され、リンクをマウスオーバーするとno-underlineで下線が非表示になります。
link_to "Rails Designer", "https://railsdesigner.com/"
しかし、link_toヘルパーの呼び出しでclass: ""のように空のクラスを指定すれば、デフォルトのスタイルは適用されなくなります。
link_to "Rails Designer", "https://railsdesigner.com/", class: ""
もちろん、これを使ってリンクのスタイルを臨時に調整できます。
🔗 秘訣3: Tailwindのgroupとdata-*属性で表示・非表示を切り替える荒技
JavaScriptをある程度使いこなしている人なら、CSSクラスにjs-をプレフィックスすることで、それらが「どこかで」「何らかの形で」JavaScriptで使われることを示すハックを見かけたことがあるかと思います。
上の秘訣と同様に、data-*属性で設定した値を使って要素を絞り込むことも可能です。今度も、Rails DesignerのNavbarComponentから取り出した実例を見てみましょう。
<nav class="group/navigation">
<ul class="hidden group-data-show-menu/navigation:block">
</ul>
</nav>
ポイントは、Tailwind CSSが提供するgroupという特殊な修飾子を使うことです。
デフォルトの<ul>要素は、hiddenによって非表示となります。
しかし以下のように、親要素である<nav>要素にdata-show-menu属性が存在している場合は、group修飾子によってblockが効くため、<ul>要素が表示されます。
<nav class="group/navigation" data-show-menu>
<ul class="hidden group-data-show-menu/navigation:block">
</ul>
</nav>
後は、data-show-menu属性を親の<nav>要素に追加するかどうかを制御するだけの、ごくシンプルで再利用可能なStimulusコントローラがあれば済むことは、容易に想像がつくでしょう。
🔗 秘訣4: Turbo Frameの内側(または外側)のスタイルを変更する
私は、Turbo Frameがモーダルダイアログを表示する優れた手法であることを学びました。リンクをクリックするだけで、ページがturbo_frameの内側にレンダリングされます。
しかし、レンダリングする場所が外部のページの場合は、同じダイアログを単独でも表示可能にしておきたいものです。そのために、シャドウや位置指定(絶対位置や固定位置)の一部を必要に応じて取り除きたいこともあります。
ありがたいことに、Tailwind CSSを使えば楽勝です。tailwindcss.config.jsファイルで、以下のようにプラグインの配列に関数を追加します。
plugins: [
// ...
function ({ addVariant }) {
addVariant("turbo-frame", "turbo-frame[src] &")
}
]
これで、Tailwind CSSでturbo-frameというカスタム修飾子が使えるようになります。このturbo-frameでも、Tailwindの他の修飾子のようにmd:やlg:などのブレークポイントを利用できます。
たとえば、モーダルコンポーネントがTurbo Frameの内側でレンダリングされたときだけshadow-xlでシャドウを設定したい場合は、以下のように書けます。
<div class="relative turbo-frame:shadow-xl"></div>
このturbo-frame[src] &も、上述のものと同様に動きます。
訳注
Tailwind v4からは、デフォルトでtailwindcss.config.jsファイルではなく、通常のCSS(tailwindcss-railsではapp/assets/tailwind/application.css)に記述することになります。
実は、v4では以下のように@variantを使えば上と同じことをずっとシンプルに書けます。
/* application.css */
@variant turbo-frame (turbo-frame[src] &);
🔗 秘訣5: Stimulusコントローラにもprivate関数を書ける
Rubyではprivate(およびprotected)メソッドを書けます。書き方はいくつかありますが、メソッドをprivateキーワードの下に移動すればprivateメソッドになります。
「Rubyクラスで公開するpublicメソッドは最小限にすべき」というプラクティスがあるのと同じ理由で、JavaScriptやStimulusのクラスでもpublic関数は最小限にしておくのがよいプラクティスです。
JavaScriptでprivate関数を作るには、以下のように関数名に#をプレフィックスするだけでできます。
class Class {
#thisIsPrivate() {
//...
}
}
私がStimulusコントローラを書くときは、たいてい以下のような構成にしています。
export default class extends Controller {
initialize() {
}
connect() {
}
disconnect() {
}
// 処理はここから下で定義する
// private
#firstPrivateFunction() {
}
#secondPrivateFunction() {
}
}
デフォルトのconnect関数とdisconnect関数を最初に配置し、Stimulusコントローラ内での実際の操作はその下に書くようにしています。// privateは機能ではなく、単なるコメントです。その下に、#をプレフィックスしたprivate関数を書いています。
// privateは見た目をRubyっぽくするためだけに置いています。こうすることで、Rubyクラスと似たような感じになります。
このようにStimulusコントローラを整理しておけば、コードが整って理解しやすくなります。詳しく知りたい方は、以下の別記事もどうぞ。
参考: How to Properly Structure Stimulus Controller | Rails Designer
🔗 秘訣6: Turbo Frameが単独で表示されないようにしておく
モーダルダイアログを多用するUIを構築したことがあれば、モーダルダイアログにスタイルを付ける作業がいかに面倒かはもうご存知かと思います。リンクをクリック→モーダルが開く→スタイルを調整→ページを更新→またリンクをクリック...といった具合です。
ありがたいことに、Railsではモーダルをautomations/newなどの単独ページとしてビルドできるので、そこでモーダルを存分に調整できます。しかしこれにはちょっとした罠があります。ユーザーがそうしたautomations/newにアクセスすると、モーダルを単独で表示できてしまうのです。アプリで単独のモーダルが必要になることは普通はないでしょう。
そういうわけで、私は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
次に、Turbo Frame内でだけ表示したいアクションに以下を書いておきます。
before_action :ensure_turbo_frame_response, only: %w[new], if: :production_environment?
こうしておけば、production環境でのみモーダルのページがTurbo Frame内で表示されるようになり、恥ずかしい思いをせずに済みます。
🔗 秘訣7: ホバー効果の遷移を遅延させる
UXのささやかな小ネタですが、きっと喜んでいただけると思います!
カードなどの要素にtransitionを追加するときは、少し遅延を設定しておきましょう。ユーザーが要素にカーソルをかざしたときにいきなり遷移効果が発動しなくなるので、いい感じになります。
Tailwind CSSクラスを使う場合は、たとえば以下のように書きます。
...
<li class="flex px-4 py-2 bg-white transition ease-in-out duration-200 delay-75 hover:bg-gray-50"></li>
...
こうしておけば、<li>要素にカーソルを一瞬かざしたぐらいでは背景色は変化せず、75msec経過したときに初めて背景色が変化するようになるので、ユーザーを無駄に煩わせずに済むようになります。
詳しくは以下の記事もどうぞ。
私がRailsアプリで使っている便利技やアイデアの一部を紹介いたしました。気に入っていただけましたか?ここに載っていない技があったらぜひお知らせください!
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。