🐬
Phlex + Literal + RubyUI で Rails の HTML を Ruby のクラスとして書く
Phlex とは?
Phlex は、HTML テンプレートを Ruby のクラスとして書くライブラリ。Ruby の設計技法をそのままビュー層に持ち込める。
どこで使う?
Inertia-Rails で React や Vue を中心に据えた構成では出番は少ないが、HTMLメールや管理画面では重宝しそう。
基本形
require "phlex"
Phlex::VERSION # => "2.4.1"
class Foo < Phlex::HTML
def view_template
p { "(A)" }
end
end
Foo.call # => "<p>(A)</p>"
引数を受け取る場合
class Foo < Phlex::HTML
def initialize(name)
@name = name
end
def view_template
p { "(#{@name})" }
end
end
Foo.new("A").call # => "<p>(A)</p>"
Foo.new("").call # => "<p>()</p>"
普通はこれでいいが、引数のバリデーションに手を出し始めると本質とは異なる処理が増えて辛くなる。
Literal で型付き属性に置き換える
Literal はクラスに型付き属性を定義するライブラリ。Phlex 専用ではないがシナジーがある。
require "literal"
class X < Literal::Data
prop :name, _String(length: 1..)
def method1
"(#{@name})"
end
end
X.new(name: "A").method1 rescue $! # => "(A)"
X.new(name: "").method1 rescue $! # => #<Literal::TypeError: Literal::TypeError>
prop のところで「name は1文字以上の文字列であること」と宣言できる。Ruby で型チェックは抵抗があるが、文字数のバリデーションまで兼ねてくれるなら嫌う理由もなかろう。
この仕組みを Phlex のビューに乗せると、引数の受け取りやバリデーションが prop 一行に集約できる。
class Foo2 < Phlex::HTML
extend Literal::Properties
prop :name, _String(length: 1..)
def view_template
p { "(#{@name})" }
end
end
Foo2.new(name: "A").call # => "<p>(A)</p>"
Foo2.new(name: "") rescue $! # => #<Literal::TypeError: Literal::TypeError>
空文字列が弾かれているのがわかる。
Rails で使う
インストール。
bundle add phlex-rails
rails g phlex:install
ビュークラスを作る。
rails g phlex:view Foo
コントローラーでは render にインスタンスを渡すだけでよい。
class UsersController < ApplicationController
def index
render Views::Foo.new
end
end
RubyUI で UI コンポーネントを揃える
RubyUI は Phlex・Tailwind CSS・Stimulus JS を土台にした UI コンポーネント集で、つまり shadcn/ui の派生版である。
まず Tailwind CSS を入れる。
bundle add tailwindcss-rails
rails tailwindcss:install
続いて RubyUI を入れる。
bundle add ruby_ui --group development --require false
rails g ruby_ui:install --force
ボタンのコンポーネントを追加する。
rails g ruby_ui:component Button
ビュークラスで利用する。
class Views::Foo < Views::Base
def view_template
Button(variant: :destructive) { "Destructive" }
end
end
Button.new としていないのは Button クラスが同名のメソッドにもなっているためである。

確認用スクリプト
開く
setup.sh
#!/bin/sh -ve
rm -fr my_app
rails new my_app
cd my_app
bundle add phlex-rails
rails g phlex:install
rails g scaffold User
rails db:migrate
rails g phlex:view Foo
cat <<'EOF' > app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
render Views::Foo.new
end
end
EOF
# open http://localhost:3000/users
# bin/dev
# ruby-ui
bundle add tailwindcss-rails
rails tailwindcss:install
bundle add ruby_ui --group development --require false
rails g ruby_ui:install --force
ruby -i -e 'puts ARGF.read.gsub(/^(@import.*tw-animate-css.*;)/, "/* \\1 */")' app/assets/tailwind/application.css
rails g ruby_ui:component button
cat <<'EOF' > app/views/foo.rb
class Views::Foo < Views::Base
def view_template
Button(variant: :destructive) { "Destructive" }
end
end
EOF
open http://localhost:3000/users
bin/dev
Discussion