`#render_in` にオプションとブロックを渡せるようにする
render_in を実装したオブジェクトへのレンダリング呼び出しで、locals などのオプションとブロックが渡せるようになりました。これにより、コンポーネントオブジェクトが呼び出し元のコンテキストに応じた柔軟なレンダリングを行えるようになります。
背景
#render_in を介したレンダリングは #36388 と #37919 で導入されましたが、その実装はオブジェクトがレンダリングに必要な情報をすべて自身で保持していることを前提としていました。そのため、呼び出し元から locals: { ... } のようなオプションを渡す手段がなく、ブロックも無視されていました。
#45432 はビューとコントローラの render でブロックをレンダラブルに渡せるよう試みましたが、オプション(locals など)の伝搬は対応していませんでした。今回の変更はその不足を解消し、オプションとブロックの両方を render_in まで一貫して伝播させます。
技術的な変更
ブロックとオプションを render_in まで届けるために、レンダリングスタック全体を横断する変更が加えられています。
ActionView::Renderer、TemplateRenderer、ActionView::Rendering#render_to_body / _render_template、AbstractController::Rendering#render_to_body といった各レイヤーのメソッドシグネチャに &block が追加され、ブロックが呼び出し元から ActionView::Template::Renderable まで伝播するようになりました。
ActionView::Template::Renderable の変更が最も中心的です。
変更前:
def render(context, *args)
@renderable.render_in(context)
rescue NoMethodError
if !@renderable.respond_to?(:render_in)
raise ArgumentError, "'#{@renderable.inspect}' is not a renderable object. It must implement #render_in."
else
変更後:
def initialize(renderable, &block)
@renderable = renderable
@block = block
end
def render(context, locals)
if @renderable.method(:render_in).arity == 1
ActionView.deprecator.warn <<~WARN
Action View support for #render_in without options is deprecated.
Change #render_in to accept keyword arguments.
WARN
@renderable.render_in(context, &@block)
else
@renderable.render_in(context, locals: locals, &@block)
end
rescue NameError
if !@renderable.respond_to?(:render_in)
raise ArgumentError, "'#{@renderable.inspect}' is not a renderable object. It must implement #render_in."
else
コンストラクタがブロックを受け取って @block に保持し、render 呼び出し時に locals と合わせて render_in へ委譲するようになりました。
ビュー側の RenderingHelper#render でも、:renderable キーを持つ Hash が渡された場合にブロックを view_renderer.render に透過させるよう条件が修正されています。
変更前:
case options
when Hash
in_rendering_context(options) do |renderer|
if block_given?
view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
else
view_renderer.render(self, options)
end
end
else
if options.respond_to?(:render_in)
options.render_in(self, &block)
変更後:
case options
when Hash
in_rendering_context(options) do |renderer|
if block_given? && !options.key?(:renderable)
view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
else
view_renderer.render(self, options, &block)
end
end
else
if options.respond_to?(:render_in)
view_renderer.render(self, renderable: options, locals: locals, &block)
block_given? の条件に !options.key?(:renderable) が加わり、:renderable を伴うブロック呼び出しがパーシャルとして誤処理されなくなりました。また、レンダラブルオブジェクトが直接渡された場合も render_in を直接呼ぶのをやめ、view_renderer.render 経由でルーティングすることで一貫性を持たせています。
さらに ActionView::Rendering#_normalize_args では、レンダラブルオブジェクトを :renderable キーに格納する際に options[:locals] = options を追加しており、positional引数で渡されたオプションが locals として render_in に届くようになっています。
これらの変更により、以下のような呼び出しがすべて動作するようになりました。
render(Greeting.new) # => "Hello, World"
render(Greeting.new, name: "Local") # => "Hello, Local"
render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local"
render(Greeting.new) { "Hello, Block" } # => "Hello, Block"
render(renderable: Greeting.new) { "Hello, Block" } # => "Hello, Block"
設計判断
既存の #render_in(view_context) シグネチャ(引数1つ)を非推奨化し、キーワード引数を受け取る新シグネチャへの移行を促す方針が採用されました。
Renderable#render 内で method(:render_in).arity == 1 を検査することで、旧シグネチャの実装を検出して非推奨警告を出しつつ動作は維持しています。これにより、既存のレンダラブルオブジェクトが壊れることなく移行期間を設けられます。rescue NoMethodError は rescue NameError に変更されており、render_in 内部で発生した NameError(例: nil.method(:render_in))をオブジェクトが render_in を持たないと誤判定するバグも同時に修正されています。
ActionController::Renderer#render は render(...) の forwarding構文に変更されており、ブロックを含む任意の引数を下位の render_to_string に透過的に渡せるようにしています。これは最小限のシグネチャ変更でブロック伝播を実現する方法として採用されています。
まとめ
この変更は、#render_in を使うコンポーネントオブジェクトがレンダリング呼び出し元の locals やブロックを受け取れるようになることで、ViewComponentのようなフレームワークとのより柔軟な統合を可能にします。レンダリングスタック全体に &block を伝播させつつ、既存のシグネチャには非推奨警告を出して後方互換性を維持するという段階的な設計判断は、Rails的な移行パスの手本といえます。