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

Rails: Controller の view_context を使って FormBuilder を作ってはいけない

🔗 前提となる環境

以下の環境で再現を確認しています。

  • 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

🔗 失敗の原因

原因は、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, {})

以上です。



CONTACT

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