🔗 前提となる環境
以下の環境で再現を確認しています。
- Ruby 3.3.4
- Rails 7.1.3.4
🔗 課題
View でレンダリングした form 要素に、Ajax でパラメータを含むHTMLを注入したいことがあります。とあるケースで、元々サーバーサイドでレンダリングするコードが存在したので、input 要素を含む同等のDOMを吐くコードをJSで一から書くよりも、既存のコードを流用したいと思いました。これはサーバーサイドで partial を render_to_string
で出力したHTMLの断片をレスポンスに載せて、それを特定の要素に流し込むことで実現ができそうです。このとき partial に渡すための ActionView::Helpers::FormBuilder
オブジェクトが必要になりますが、どのようにしてインスタンス化すればよいでしょうか。
🔗 TL;DR
🔗 ダメだった方法
Stack Overflow に質問と回答がありました。
- 方法1
ruby on rails - Passing ActionView::Helpers::FormBuilder to a partial - Stack Overflow
ruby - Rails render_to_string with partial that requires a form builder object? - Stack Overflow
- 方法2
ruby - Rails render_to_string with partial that requires a form builder object? - Stack Overflow
結論から言うと、どちらもダメでした。一見すると動くのですが、問題は、#fields_for
を使ったときのことでした。
🔗 サンプルコード
本来は Ajax でやることなのですが、検証内容と関係がないので、JSは割愛して render_to_string
の結果を View に直接書き込みます。
Controller に以下のように書きます。
class TestRecordsController < ApplicationController
def index
@form = TestForm.new
@form.test_records = [
TestRecord.new(id: 1, name: 'aaa'),
TestRecord.new(id: 2, name: 'bbb'),
TestRecord.new(id: 3, name: 'ccc'),
]
f = ActionView::Helpers::FormBuilder.new(@form.model_name.param_key, @form, view_context, {})
@html = render_to_string(partial: 'test_records/partial', locals: { f: })
end
end
TestRecord
は割愛します。普通のモデルクラスだと思ってください。TestForm
の定義はテキトーに、こんな感じです。fields_for
するための最小限になっています。
class TestForm
include ActiveModel::Model
attr_accessor :test_records
def test_records_attributes=(attributes)
end
end
続いて、メインの View (index.html.haml) に次のように書きます。ここでは @html
が出せればどうでもいいです。
= form_with model: @form, url: [:root], method: :get do |f|
= @html
最後に、partial に次のように書きます。渡ってきた f
の #fields_for
を呼んでいるのがポイントです。実際のコードでは、メインの View からも同じ partial を render
しています。
%ul
= f.fields_for :test_records do |ff|
%li
= ff.hidden_field :id
%p= ff.object.name
🔗 期待する結果
当然ですが、こうなるのが正しいです。
- aaa
- bbb
- ccc
🔗 実際の結果
こうなりました。
- aaa
- bbb
- ccc
- aaa
- aaa
- bbb
- aaa
- bbb
- ccc
- aaa
🔗 失敗の原因
原因は、ActionView::Base
が内部で保持している ActionView::OutputBuffer
の取り違えが発生していることでした。詳しくは追い切れていないので雑な理解なのですが、Controller から ActionView::Helpers::FormBuilder
のコンストラクタに view_context
を渡した結果、fields_for
のブロックからのHTML出力が想定と異なる ActionView::OutputBuffer
に向かってしまっていたようです。
🔗 うまくいった方法
Controller で直接 ActionView::Helpers::FormBuilder
を作らず、ラッパーの partial をもう1個作り、その中で view_context
ではなく self
を渡す。Controller からはラッパーの partial を render_to_string
することでうまくいきました。
ラッパーの partial はこうです。
= render 'test_records/partial', f: ActionView::Helpers::FormBuilder.new(@form.model_name.param_key, @form, self, {})
以上です。