10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソニックガーデン プログラマAdvent Calendar 2024

Day 15

ViewComponent で Stimulus を管理する方法 - esbuild版

Last updated at Posted at 2024-12-14

この記事は、ソニックガーデン プログラマ 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.jsapplication.register を手作業で追加するのは大変なので stimulus:manifest:update を実行することで自動登録されるようにします。

以下の Discussion で stimulus-rails で用意されている rakeタスクにモンキーパッチするアイデアが投稿されていたので、これを使わせてもらいます。
https://github.com/ViewComponent/view_component/discussions/1312

以下のコードを lib/tasks/stimulus_tasks.rake に保存します。

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 を作る

app/components/clipboard/component.html.haml
%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
app/components/clipboard/component.rb
class Clipboard::Component < ViewComponent::Base  
  def initialize(text:)  
    @text = text  
  end  
end
app/components/clipboard/component_controller.js
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します。

CleanShot 2024-12-14 at 12.10.22@2x.png

使えるようになりました 🎉

haml内の記述を楽にする

このままでも良いのですが、コンポーネント毎に閉じ込める構成をとったために、haml内に --component の記述が出てきてちょっとノイズに感じてしまいます。
せっかく ViewComponent を使っているのですから、ruby側で工夫してみましょう。

1. 親クラスに便利メソッドを定義

各コンポーネントクラスの親クラスを定義します。

app/components/application_comonent.rb
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 で継承するように変更します。

app/components/clipboard/component.rb
-class Clipboard::Component < ViewComponent::Base
+class Clipboard::Component < ApplicationComponent
  def initialize(text:)  
    @text = text  
  end  
end

3. viewに適用

app/components/clipboard/component.html.erb
%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

10
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?