Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: importmap-rails + propshaft 環境でapp/components/配下の Stimulusjs ファイルを読み込む

環境

以下のセットアップを前提としています。つまりnodeを使わない方法です。

  • Rails 8.x
    • importmap-rails
    • propshaft
    • tailwindcss-rails
  • Ruby 4.0.x
  • ViewComponent v4.2.0

ViewComponentを使うなら、コンポーネントで使うStimulusコントローラ(やCSS)もコンポーネントと同じ場所に配置するだけで自動的に読み込まれるようにしたいですよね。

app/components
├── application_component.rb
├── shared
│   ├── tooltip_component.html.erb
│   ├── tooltip_component.rb
│   ├── tooltip_controller.js -- 置くだけでオートロードしたい
    ...

もちろん、グローバルに使うStimulusコントローラなら普通にapp/javascript/の下に置いています。

app/javascript
├── application.js
└── controllers -- importmap-rails環境ではオートロードされる
    ├── application.js
    ├── index.js
    ├── mobile_menu_controller.js
    ├── regex_highlight_controller.js
    └── textarea_autogrow_controller.js

以下の記事にも書きましたが、importmap-railsを使っていれば、app/javascript/controllers/以下に配置したカスタムStimulusコントローラは動的に読み込まれるので、Stimulusコントローラを手動でindex.jsに登録する必要はありません。

Rails 7: importmap-railsとjsbundling-railsでのStimulusの扱いの違い

しかし以下を見ると、Railsでpropshaftを使っている場合にapp/components/以下に置いたJavaScriptファイルを読み込ませるための設定をViewComponentの人たちがドキュメント化しようとしたものの、さまざまな困難からドキュメント化を諦めてしまったようです。

issue: Document a propshaft approach to JS/CSS by jkotchoff · Pull Request #2160 · ViewComponent/view_component


@jkotchoff: このプルリク(およびアセット関連の他のトピック)についてViewComponentの他のメンテナーたちと協議した結果、ドキュメントからこのセクションを完全に削除することが決定された。このセクションを最新に保つことが困難であり、コミュニティ内でのコンセンサスや規約との整合性が取れないことで価値が目減りしているため。
#2160コメントより

Stimulusファイルをapp/components/の下に配置するための基本設定

ここからが本題です。

複数のAIとやりとりを重ねた結果、以下の2つを追加することで、app/components/以下に配置したJSファイルを動的に読み込めることを確かめました。

# config/importmap.rb
# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
+pin_all_from "app/components", under: "components", to: ""

なお、pin_all_fromの対象は_controller.jsファイルのみなので、Stimulus以外のjsや、.rbや.html.erbは自動読み込みされません。

// app/javascript/controllers/index.js
// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

+// For ViewComponents
+eagerLoadControllersFrom("components", application)

上のissue #2160で議論されていたようなconfig.assets.paths += Dir.glob("app/components/**/*.js")を書く必要はありませんでした。

参考: Stimulusコントローラをlazy loadingすることも可能

eagerLoadControllersFromの代わりにlazyLoadControllersFromを使えば、data-controller-*が読み込まれるまでStimulusコントローラの読み込みを遅延できます。もちろん両方使っても構いません。

// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
-import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
+import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
-eagerLoadControllersFrom("controllers", application)
+lazyLoadControllersFrom("controllers", application)

// For ViewComponents
-eagerLoadControllersFrom("components", application)
+lazyLoadControllersFrom("components", application)

Stimulusコントローラが増えてきたときに使えそうです。

その代わり、現時点ではDevToolsのコンソールにコントローラの数だけエラーが表示されてしまいます(害はありませんが)。

ただしパス指定には注意が必要

特にapp/components/のサブディレクトリにあるStimulusコントローラへのパス指定は面倒なポイントであり、AIも間違えがちな部分でした。

  • 1. コントローラー名の命名規則
    app/components/以下に配置したコントローラーは、パス構造がコントローラー名のプレフィックスになる(プレフィックスは--で指定すること)

これは、@hotwired/stimulus-loadingのeagerLoadControllersFrom("components", application)がディレクトリ構造をそのままコントローラ識別子に変換しているためです。

ファイルパスの例 コントローラー名の指定方法
app/components/shared/tooltip_controller.js shared--tooltip
app/components/patterns/category_select_controller.js patterns--category-select
app/components/lookbook/copy_button_component_controller.js lookbook--copy-button-component
  • 2. HTML でのdata-*属性の対応
    Stimulusに関連するすべてのdata-*属性にコントローラー名のプレフィックスが必要(プレフィックスは--で指定すること)

以下はapp/components/shared/にtooltip_controller.jsを配置した場合のdata-*の書き方です。

# ❌ 間違い(これはapp/javascript/controllers/ に置いた場合の書き方)
data: {
  controller: "tooltip",
  tooltip_target: "tooltip",
  action: "mouseenter->tooltip#show"
}

# ✅ 正しい(app/components/shared/に置いた場合)
data: {
  controller: "shared--tooltip",
  "shared--tooltip_target": "tooltip",
  action: "mouseenter->shared--tooltip#show"
}

Railsのdata:ハッシュでは_-に変換されるため、HTML上ではdata-shared--tooltip-target="tooltip"になります。

  • 3. 対応が必要な属性一覧
属性の種類 例(shared--tooltipの場合)
controller data-controller="shared--tooltip"
target data-shared--tooltip-target="tooltip"
action data-action="click->shared--tooltip#show"
value data-shared--tooltip-error-id-value="1"
class data-shared--tooltip-active-class="visible"

CSSの制限事項

なお、CSSも同じ要領でapp/components/の下にコンポーネント専用のCSSファイルを置けるかどうかを試してみました。

しかし私の場合、Tailwind v4になったことでtailwind.config.jsを使わないようにしています(後方互換性のために引き続きtailwind.config.jsを使うことは一応可能ですが、推奨されていません)。

tailwind.config.jsなしの場合、app/components/の下にあるCSSを自動で読み込むことまではできず、明示的にCSSへのパスを@importする必要があるらしいことがわかりました。

そこまでする必要を感じられなかったので、CSSについてはapp/components/からの自動読み込みはやめました。

最後に

小規模なアプリならそこまでしなくても普通にapp/javascript/controllers/以下にStimulusコントローラを置けば済むと思いますが、アプリが大きくなったときほどコンポーネントに対応するStimulusコントローラを見つけやすくなるかなと思います。

関連記事

Rails 7: importmap-railsとjsbundling-railsでのStimulusの扱いの違い

Rails: パーシャルよりもViewComponentを選ぶべき理由(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。