2024-12-03

simple_formとStimulusを併用していて地味に詰まったポイント

simple_form
Hotwire
Stimulus
Ruby on Rails

TL;DR

  • simple_formでケバブケースに変換されたdata-target属性と、Stimulusでtargetを参照する際に使用される識別子のミスマッチでtargetが参照できなくなっていた
    • simple_formは属性名をHTMLに合わせてスネークケースからケバブケースに変換してくれる
    • Stimulusで識別子として登録されるのはControllerのファイル名=スネークケース

https://stimulus.hotwired.dev/reference/controllers#modules

  • StimulusのController名が2単語以上で構成される場合、input_htmlでdata属性のハッシュではなく、文字列で"data-controller_name-target"のように属性を設定する必要がある
- input_html: {
-   data: {
-     "controller_name-target": "target",
-     action: "change->controller_name#action"
-   }
- }
+ input_html: {
+   "data-controller_name-target": "target",
+   data: { action: "change->controller_name#action" }
+ }

地味に詰まったポイント

Stimulusでは、参照したい要素に名前を付けてControllerで参照することができます。

user_role_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
  static targets = ['select'];

  connect() {
    // data-user_role-target属性値が"select"の要素が取得できる
    this.selectTarget.doSomething();
    // ...
  }
}

Viewの方で参照したい要素にdata-controller_name-target属性を設定しますが、最初はケバブケースで指定していタコとにより、上記connect関数内でtargetが参照できなくて困っていました。

_form.html.erb
<%= simple_form_for @user do |f| %>
  <%= f.input :name %>
  <%= f.association :roles, collection: Role.all,
    input_html: {
      class: "select2 browser-default",
      data: {
        "user-role-target": "select",
        action: "change->user_role#hoge"
      }
    }
  %>
  <%= f.button :submit %>
<% end %>

そこでControllerのファイル名と合わせてスネークケースに修正すれば動くのでは?と以下のように修正。

_form.html.erb
<%= simple_form_for @user do |f| %>
  <%= f.input :name %>
  <%= f.association :roles, collection: Role.all,
    input_html: {
      class: "select2 browser-default",
      data: {
        "user_role-target": "select",
        action: "change->user_role#hoge"
      }
    }
  %>
  <%= f.button :submit %>
<% end %>

原因のあたりの付け方は悪くありませんでしたが、これでも動かず。
Chrome DevToolsを確認したところ、simple_formの仕様?で属性をケバブケースに変換してくれていることがわかりました。

最終的に辿り着いたのが、以下のような形です。
最悪の場合、部分的にsimple_formを使わずに自前で書くことも考えましたが、回避方法があってよかったです。

_form.html.erb
<%= simple_form_for @user do |f| %>
  <%= f.input :name %>
  <%= f.association :roles, collection: Role.all,
    input_html: {
      class: "select2 browser-default",
      "data-user_role-target": "select",
      data: {
        action: "change->user_role#hoge"
      }
    }
  %>
  <%= f.button :submit %>
<% end %>

補足

ちなみにtarget名はControllerのプロパティとして直接マッピングされるためキャメルケースにする必要があります。
命名規則が入り混じっていて非常に混乱する...。

おわりに

明日は @yubachiri さんによる プログラミング言語を作る 本を読んだところまでまとめてみる です。