概要
RailsアプリケーションにJSで動的な動作がちょっとだけ必要なとき、Stimulusを使うと簡単にJSを実装できます。しかし、Stimulusとerbとの間でデータを受け渡すときには、その方法に注意しなければなりません。特に、targetsとvaluesの受け渡し方は違うので注意が必要です。
注意点1: targetとvalueで渡し方が違うことに注意
問題
先日の開発で、Stimulusを使ってRailsのerbに動的な動作を実装していました。下記のような実装だったのですが、どうもvaluesがうまくJS側に渡せていないようです。
例えば下記のような例があったとします。
各ユーザに設定するメッセージを入力する欄があり、その初期値としてコントローラから共通の値を入れる場合です。
<div data-controller="hoge">
<p>各ユーザのメッセージ</p>
<% @users.each do |user| %>
<div>
<div><%=user.name%>さん</div>
<!--各ユーザのメッセージを設定するinput-->
<input type="text"
data-hoge-target="userMessage"
data-hoge-initial-message-value="<%=@initial_message%>"> <!--全ユーザ共通の初期値を渡してるつもり-->
<button>送信</button>
</div>
<% end %>
</div>
上記の書き方ですが、valueの受け渡し方に問題があります。これではstimulusのcontroller側にvaluesが渡りません。この原因がわからなくて先日は何時間か時間を浪費してしまいました。
対処内容
valuesはdata-controller指定箇所と同じタグに書く
やはり公式ドキュメントはよく見なければなりませんね。
公式ドキュメントのValuesの説明を見ると、valuesはdata-controllerを記述したタグと同じ場所に置くと書いてあります。
つまり正しい実装はこうなります。
<!--data-controllerと同じ場所でvaluesを受け渡す-->
<div data-controller="hoge"
data-hoge-initial-message-value="<%=@initial_message%>">
<p>各ユーザのメッセージ</p>
<% @users.each do |user| %>
<div>
<div><%=user.name%>さん</div>
<!--各ユーザのメッセージを設定するinput-->
<input type="text"
data-hoge-target="userMessage">
<button>送信</button>
</div>
<% end %>
</div>
targetsはJS側に渡したいタグに書く
なぜこのようなミスをしてしまったのか振り返ってみると、targetの受け渡し方とvaluesの受け渡し方を混同してしまったためでした。targetsはstimulus上で操作の対象となるタグを指定しますが、valuesはそうではないことに気を付けるべきでした。
注意点2: 従来のdata属性と混在させない
問題
例えば、複数のユーザのリストをerbとして描画した後、各ユーザに固有の値をJS側に渡したいときは、どうすればよいでしょうか。安直に考えたら、erbでeachを回している中でdata属性を作ってJSに渡すことを考えるでしょう。
<!--data-controllerと同じ場所でvaluesを受け渡す-->
<div data-controller="hoge"
data-hoge-initial-message-value="<%=@initial_message%>">
<p>各ユーザのメッセージ</p>
<% @users.each do |user| %>
<div>
<div><%=user.name%>さん</div>
<!--各ユーザのメッセージを設定するinput-->
<input type="text"
data-hoge-target="userMessage"
data-user-id="<%=user.id%>"> <!--user固有の値を受け渡す-->
<button>送信</button>
</div>
<% end %>
</div>
データを受け取るJS側の記述はこのようになります。
// hoge_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ['userMessage']
static values = {
initialMessage: String
}
hoge () {
// **Targetsで一致する要素すべてを取得
const userInputs = this.userMessageTargets
userInputs.forEach( (input) => {
// 各Idへの処理
const userId = input.dataset.userId
...(省略)
})
}
...(省略)
}
対処内容
これもまた、Stimulusを使う上ではあまりよくありません。Stimulusの公式ドキュメントでは、従来のdata属性を使っていた部分はvaluesに置き換える方法が書かれています。
先ほどのdata属性をvaluesに置き換えるとこういう感じになります。
// hoge_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
initialMessage: String
userIds: Array // data属性だったものをvaluesで受け取る
}
hoge () {
// valuesの中身からuserIdを取得して操作
this.userIdsValue.forEach( (userId) => {
// 各Idへの処理
...(省略)
})
}
...(省略)
}
erb側はこうなります。なお、配列はJSONではJson.parse()でデコードされるらしい(参考)ので、渡すときは配列を .to_json するのがワンポイントですね。
<!--data-controllerと同じ場所でvaluesを受け渡す-->
<div data-controller="hoge"
data-hoge-initial-message-value="<%=@initial_message%>"
data-hoge-user-ids-value="<%=@users.pluck(:id).to_json%>"> <!--JSに渡す配列の中身をJSONにして渡す-->
<p>各ユーザのメッセージ</p>
<% @users.each do |user| %>
<div>
<div><%=user.name%>さん</div>
<!--各ユーザのメッセージを設定するinput-->
<input type="text"
data-hoge-target="userMessage"> <!--userのIDを渡したdata属性を削除-->
<button>送信</button>
</div>
<% end %>
</div>
正直に言うと一部Geminiに聞いた受け売りも入ってはいますが、Stimulusではdata属性ではなく、valuesを使ったほうが良いと思われるのは下記の理由からです。
- 実装方法に一貫性を持たせ、可読性に影響をきたさないようにする
- Stimulusの便利な機能を使える
実装方法に一貫性を持たせる
上記のコードを見ると、JS側で使われるvaluesやtargetはすべてコントローラの上側に集約されているので、erb側と関係している値が一目でわかるようになります。erb上のどこかしらで知らないうちに共有されている値がなくなるということですね。
Stimulusの便利な機能を使える
上記の公式ドキュメントを見るとStimulusには下記のような2つの便利機能があります。
- valuesを宣言するだけでvaluesの値が変わった時のコールバック関数が自動で定義される
- しかもこの関数、Controllerの中での代入なども検知してくれるとのことです。つまり、今まであったような「値をコードから更新したらイベントを手動で発火しなければならない」という面倒で不安定なことをしなくてもよくなります。
- valuesの変化をとらえるコールバック関数で変わる前後の値が引数にわたってくる
まとめ: Stimulusは公式ドキュメントを見て使いこなす
Stimulusはerbと連携させるときは、タイポや勘違いによってエラーを引き起こしやすいです。設定する値がちゃんとわたって正しく動作しているかは統合テストなどでも検査しましょう。また、StimulusもJSなので、古い書き方をしようと思えばできてしまいます。しかし、Stimulusならではの便利機能をうまく使いこなすことで、より分かりやすく、シンプルにコードを書くことができます。