From ea4b767155b1a0066129ca515486e86c8acd5d41 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Fri, 24 Apr 2026 10:04:53 -0600 Subject: [PATCH 1/6] Fix yielded content rendered at wrong location with form helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a component renders a partial that uses a Rails form helper (like form.label) with a yielding block, the yielded content was rendered outside the helper's tag instead of inside it. Root cause: form_with creates form builders with @template_object pointing to the component. When form.label calls capture through the component during partial rendering, the component's @output_buffer (set once in render_in) is stale — Rails' _run has already replaced the view context's output buffer with a fresh one for the partial. The capture reads from the wrong buffer while the block writes to the partial's buffer, causing the content to leak. Fix: Override capture in ViewComponent::Base to sync @output_buffer with view_context.output_buffer before delegating to super. Fixes #2617 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/view_component/base.rb | 16 ++++++++++++++++ .../partial_with_yield_form_component.html.erb | 5 +++++ .../partial_with_yield_form_component.rb | 2 ++ .../views/shared/_yielding_form_partial.html.erb | 3 +++ test/sandbox/test/rendering_test.rb | 4 ++++ 5 files changed, 30 insertions(+) create mode 100644 test/sandbox/app/components/partial_with_yield_form_component.html.erb create mode 100644 test/sandbox/app/components/partial_with_yield_form_component.rb create mode 100644 test/sandbox/app/views/shared/_yielding_form_partial.html.erb diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index 5a2a67dcd..f9bec31c1 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -278,6 +278,22 @@ def render(options = {}, args = {}, &block) end end + # Sync @output_buffer with the view context's current output buffer before + # capturing. Form helpers create builders with @template_object pointing to + # the component. When those builders later call capture inside a partial + # (whose _run allocated a fresh OutputBuffer on the view context), the + # component's stale @output_buffer would capture from the wrong buffer. + # Temporarily switching to the view context's buffer keeps both in sync. + # + # @private + def capture(*, **, &block) + old_output_buffer = @output_buffer + @output_buffer = view_context.output_buffer if view_context + super + ensure + @output_buffer = old_output_buffer + end + # The current controller. Use sparingly as doing so introduces coupling # that inhibits encapsulation & reuse, often making testing difficult. # diff --git a/test/sandbox/app/components/partial_with_yield_form_component.html.erb b/test/sandbox/app/components/partial_with_yield_form_component.html.erb new file mode 100644 index 000000000..4de578dc1 --- /dev/null +++ b/test/sandbox/app/components/partial_with_yield_form_component.html.erb @@ -0,0 +1,5 @@ +<%= form_with url: '/' do |form| %> + <%= render "shared/yielding_form_partial", form: do %> + world + <% end %> +<% end %> diff --git a/test/sandbox/app/components/partial_with_yield_form_component.rb b/test/sandbox/app/components/partial_with_yield_form_component.rb new file mode 100644 index 000000000..e2ea32f31 --- /dev/null +++ b/test/sandbox/app/components/partial_with_yield_form_component.rb @@ -0,0 +1,2 @@ +class PartialWithYieldFormComponent < ViewComponent::Base +end diff --git a/test/sandbox/app/views/shared/_yielding_form_partial.html.erb b/test/sandbox/app/views/shared/_yielding_form_partial.html.erb new file mode 100644 index 000000000..d4f2ca982 --- /dev/null +++ b/test/sandbox/app/views/shared/_yielding_form_partial.html.erb @@ -0,0 +1,3 @@ +<%= form.label :something do %> + <%= yield %> +<% end %> diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb index ee98488ae..b9169a913 100644 --- a/test/sandbox/test/rendering_test.rb +++ b/test/sandbox/test/rendering_test.rb @@ -1379,6 +1379,10 @@ def test_render_partial_with_yield assert_text "hello world", exact: true, normalize_ws: true end + def test_render_partial_with_yield_form + assert_includes render_inline(PartialWithYieldFormComponent.new).css('label').to_html, 'world' + end + def test_render_partial_with_yield_and_method_call render_inline(PartialWithYieldAndMethodCallComponent.new) assert_text "hello world", exact: true, normalize_ws: true From 726cee5a43d3c550cd3677e9e4f583dae21a38e9 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Thu, 7 May 2026 11:55:13 -0600 Subject: [PATCH 2/6] cleanup --- lib/view_component/base.rb | 4 ++-- test/sandbox/test/rendering_test.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index f9bec31c1..0b1eccfbc 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -286,10 +286,10 @@ def render(options = {}, args = {}, &block) # Temporarily switching to the view context's buffer keeps both in sync. # # @private - def capture(*, **, &block) + def capture(...) old_output_buffer = @output_buffer @output_buffer = view_context.output_buffer if view_context - super + super(...) ensure @output_buffer = old_output_buffer end diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb index b9169a913..c559841ef 100644 --- a/test/sandbox/test/rendering_test.rb +++ b/test/sandbox/test/rendering_test.rb @@ -1380,7 +1380,7 @@ def test_render_partial_with_yield end def test_render_partial_with_yield_form - assert_includes render_inline(PartialWithYieldFormComponent.new).css('label').to_html, 'world' + assert_includes render_inline(PartialWithYieldFormComponent.new).css("label").to_html, "world" end def test_render_partial_with_yield_and_method_call From e1681207362e067f9012477711a28f12ddff6d49 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Thu, 7 May 2026 11:57:27 -0600 Subject: [PATCH 3/6] no need for ... --- lib/view_component/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index 0b1eccfbc..a4c221e2b 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -289,7 +289,7 @@ def render(options = {}, args = {}, &block) def capture(...) old_output_buffer = @output_buffer @output_buffer = view_context.output_buffer if view_context - super(...) + super ensure @output_buffer = old_output_buffer end From 0d0302f9151db0864d9f1586e7f0d9a85a0ebdd1 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Thu, 7 May 2026 11:59:57 -0600 Subject: [PATCH 4/6] lint --- .../app/components/partial_with_yield_form_component.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sandbox/app/components/partial_with_yield_form_component.html.erb b/test/sandbox/app/components/partial_with_yield_form_component.html.erb index 4de578dc1..37359983c 100644 --- a/test/sandbox/app/components/partial_with_yield_form_component.html.erb +++ b/test/sandbox/app/components/partial_with_yield_form_component.html.erb @@ -1,4 +1,4 @@ -<%= form_with url: '/' do |form| %> +<%= form_with url: "/" do |form| %> <%= render "shared/yielding_form_partial", form: do %> world <% end %> From 544468614ee613b524a814bd9d20a5501661c2fd Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Thu, 7 May 2026 12:56:43 -0600 Subject: [PATCH 5/6] add changelog --- docs/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 33b31ae91..3898b079b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,10 @@ nav_order: 6 ## main +* Fix yielded content rendered at wrong location when using form helpers. + + *Joel Hawksley*, *Markus* + ## 4.8.0 * Add `compile.view_component` ActiveSupport::Notifications event for eager compilation at boot time. From dc9e726b83002f53a75474862a7d4ed0d70c75d7 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Thu, 7 May 2026 13:35:10 -0600 Subject: [PATCH 6/6] Apply suggestion from @joelhawksley --- docs/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0bfb835dd..c44d6e85e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -28,7 +28,6 @@ nav_order: 6 *Joel Hawksley* - ## 4.8.0 * Add `compile.view_component` ActiveSupport::Notifications event for eager compilation at boot time.