🐬

Phlex + Literal + RubyUI で Rails の HTML を Ruby のクラスとして書く

に公開

Phlex とは?

https://www.phlex.fun/

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 で型付き属性に置き換える

https://literal.fun/

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 コンポーネントを揃える

https://www.rubyui.com/

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