diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 592c186bf..349ed2d94 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,10 @@ nav_order: 6 ## main +* Fix stale content cache when slots are accessed before `render_in`. + + *Jared Armstrong* + * Add rubocop-view_component to resources. *Andy Waite* diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index b251991d7..f82fb6d46 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -131,6 +131,7 @@ def render_in(view_context, &block) end @__vc_content_evaluated = false + remove_instance_variable(:@__vc_content) if defined?(@__vc_content) @__vc_render_in_block = block @view_context.instance_variable_set(:@virtual_path, virtual_path) diff --git a/test/sandbox/app/components/slot_with_content_block_component.rb b/test/sandbox/app/components/slot_with_content_block_component.rb new file mode 100644 index 000000000..dcd9e3c2f --- /dev/null +++ b/test/sandbox/app/components/slot_with_content_block_component.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class SlotWithContentBlockComponent < ViewComponent::Base + renders_one :header + + def call + out = +"" + out << content_tag(:div, header, class: "header") if header? + out << content_tag(:div, content, class: "body") if content? + out.html_safe + end +end diff --git a/test/sandbox/test/slotable_test.rb b/test/sandbox/test/slotable_test.rb index d13be3692..778ece663 100644 --- a/test/sandbox/test/slotable_test.rb +++ b/test/sandbox/test/slotable_test.rb @@ -822,6 +822,23 @@ def test_slot_name_methods_are_not_shared_accross_components assert_not_equal SlotsComponent.instance_method(:title).owner, SlotNameOverrideComponent::OtherComponent.instance_method(:title).owner end + def test_slot_predicate_before_render_does_not_poison_content_cache + component = SlotWithContentBlockComponent.new + component.with_header { "My Header" } + + # Accessing a slot predicate before render_in triggers content evaluation + # via __vc_get_slot. Without the fix, this caches a nil @__vc_content that + # persists through render_in, causing the content block to be silently ignored. + assert component.header? + + render_inline(component) do + "Body content from block" + end + + assert_selector(".header", text: "My Header") + assert_selector(".body", text: "Body content from block") + end + def test_allows_marking_slot_as_last render_inline(LastItemComponent.new) do |component| component.with_item("animal")