この記事は、ソニックガーデン プログラマ Advent Calendar 2024の15日目の記事です。
はじめに
Rails 環境で esbuild を使いバンドルしている場合、ViewComponent を活用して Stimulus を管理する際にひと手間が必要でした。本記事ではその手順をまとめます。
この構成は本番環境で約1年間運用しており、大きなトラブルもなく安定しています。また、各コンポーネントに関連するファイルを単一ディレクトリ内にまとめることで、より分かりやすく管理できるようにしています。
今回は Rails 8.0 と ViewComponent 3.20.0 の環境を用いて解説します。
目指す構成
次のようなディレクトリ構成を目指します。
app
└── components
└── clipboard
├── component.html.haml
├── component.rb
└── component_controller.js
-
components
配下 に そのコンポーネントに閉じた形でhaml
,rb
,js
を配置 (この例ではClipboard
コンポーネント)
セットアップ
以下は、ViewComponent と esbuild の基本的なセットアップが済んでいる状態を前提とします。ここでは、ディレクトリごとに Stimulus を効率的に管理する方法を構築します。
stimulus:manifest:update
で Controller が登録されるようにする
app/javascript/controllers/index.js
に application.register
を手作業で追加するのは大変なので stimulus:manifest:update
を実行することで自動登録されるようにします。
以下の Discussion で stimulus-rails
で用意されている rakeタスクにモンキーパッチするアイデアが投稿されていたので、これを使わせてもらいます。
https://github.com/ViewComponent/view_component/discussions/1312
以下のコードを lib/tasks/stimulus_tasks.rake
に保存します。
require 'stimulus/manifest'
namespace :stimulus do # rubocop:disable Metrics/BlockLength
namespace :manifest do
task update: :environment do
components_manifest = Stimulus::Manifest.generate_from(Rails.root.join('app/components'))
controllers_manifest = Stimulus::Manifest.generate_from(Rails.root.join('app/javascript/controllers'))
components_manifest.each do |component|
component.gsub!(/from "\./, 'from "./../../components')
end
Rails.root.join('app/javascript/controllers/index.js').open('w+') do |index|
index.puts '// This file is auto-generated by ./bin/rails stimulus:manifest:update'
index.puts '// Run that command whenever you add a new controller or create them with'
index.puts '// ./bin/rails generate stimulus controllerName'
index.puts
index.puts %(import { application } from "./application")
index.puts components_manifest
index.puts controllers_manifest
end
end
end
end
この Rake タスクを実行すると、app/javascript/controllers/index.js
に自動的にコードが生成され、コンポーネントディレクトリ内の Stimulus Controller が登録されます。
以下は、モンキーパッチ前の stimulus-rails
のtaskです。気になる方は差分を確認してみてください。
https://github.com/hotwired/stimulus-rails/blob/main/lib/tasks/stimulus_tasks.rake
Clipboardコンポーネントを作成
今回はサンプルとして Stimulus Handbook の Clipboard コントローラーを参考にして作ってみます。
https://stimulus.hotwired.dev/handbook/building-something-real
1. app/components/clipboard 配下に haml, rb, js を作る
%div{ data: { controller: 'clipboard--component' } }
PIN:
%input{ data: { 'clipboard--component-target': 'source' }, type: 'text', value: @text }
%button{ data: { action: 'clipboard--component#copy' } } Copy to Clipboard
class Clipboard::Component < ViewComponent::Base
def initialize(text:)
@text = text
end
end
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['source']
copy() {
navigator.clipboard.writeText(this.sourceTarget.value)
alert(`${this.sourceTarget.value} をコピーしました`)
}
}
同じディレクトリ app/components/clipboard
で haml, rb, js を管理できるので分かりやすいですね。
2. manifestのupdate
bin/rails stimulus:manifest:update
このままでは app/components/clipboard/component_controller.js
を読み込んでくれないので、rakeタスクを実行して manifestをupdate します。
app/javascript/controllers/index.js
に以下のような差分ができます。
+import Clipboard__ComponentController from "./../../components/clipboard/component_controller"
+application.register("clipboard--component", Clipboard__ComponentController)
3. コンポーネントを利用する側の記述
<%= render(Clipboard::Component.new(text: 'Hello')) %>
コンポーネントを使いたいところで、renderします。
使えるようになりました 🎉
haml内の記述を楽にする
このままでも良いのですが、コンポーネント毎に閉じ込める構成をとったために、haml内に --component
の記述が出てきてちょっとノイズに感じてしまいます。
せっかく ViewComponent を使っているのですから、ruby側で工夫してみましょう。
1. 親クラスに便利メソッドを定義
各コンポーネントクラスの親クラスを定義します。
class ApplicationComponent < ViewComponent::Base
private
def controller_name
self.class.name.underscore.tr('_', '-').gsub('/', '--')
end
def target(value)
{ "#{controller_name}_target" => value }
end
def action(value)
{ action: "#{controller_name}##{value}" }
end
end
2. component.rb の編集
先に作った Clipboard::Component
で継承するように変更します。
-class Clipboard::Component < ViewComponent::Base
+class Clipboard::Component < ApplicationComponent
def initialize(text:)
@text = text
end
end
3. viewに適用
%div{ data: { controller: controller_name } }
PIN:
%input{ data: { **target('source') }, type: 'text', value: @text }
%button{ data: { **action('copy') } } Copy to Clipboard
すっきり書けました。
まとめ
本記事では、ViewComponent を活用して Stimulus を効率的に管理する方法を紹介しました。この方法により、コードの可読性と保守性が向上し、新しいメンバーでもスムーズにプロジェクトに加わることができるのではないでしょうか。
補足
2024/12/15 時点で ViewComponent 公式には webpack を用いた方法しか記載されていませんでした。
https://viewcomponent.org/guide/javascript_and_css.html#stimulus
また、Propshaft を用いた方法はプルリクエストが出ているので、そのうち公式ドキュメントで見られるようになると思います。
https://github.com/ViewComponent/view_component/pull/2160