From 5abc33bffd0789fa110cd2b551f5870e08236ccd Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Sun, 29 Sep 2024 09:51:28 -0400 Subject: [PATCH] Add configuration to treat content as a slot Like discussed many times before, this finally adds content as a slot to ViewComponent so that we can begin separating the block provided when rendering from content for performance and ergonomic reasons. This adds two new methods: * `content_is_a_slot!` - This makes the component _and its descendants_ treat `content` as a slot, and the block provided to render calls _is always executed_. * `do_not_use_content_as_a_slot!` - This keeps the existing behavior where `content` is lazily evaluated and is used to return content. There's likely more implications from this change that need to be walked through (like how slots are used, and easily setting content) before this can be merged. --- lib/view_component/base.rb | 24 ++++++++ lib/view_component/slotable.rb | 16 ++--- .../content_as_slot_component.html.erb | 5 ++ .../components/content_as_slot_component.rb | 3 + .../content_not_a_slot_component.rb | 3 + test/sandbox/test/slotable_test.rb | 58 +++++++++++++++++++ 6 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 test/sandbox/app/components/content_as_slot_component.html.erb create mode 100644 test/sandbox/app/components/content_as_slot_component.rb create mode 100644 test/sandbox/app/components/content_not_a_slot_component.rb diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index 9996807e2..88e6db55d 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -103,6 +103,13 @@ def render_in(view_context, &block) @__vc_content_evaluated = false @__vc_render_in_block = block + if self.class.send(:__vc_content_is_a_slot?) + @__vc_content_evaluated = true + if __vc_render_in_block_provided? + view_context.capture(self, &@__vc_render_in_block) + end + end + before_render if render? @@ -549,6 +556,10 @@ def render_template_for(variant = nil, format = nil) child.instance_variable_set(:@__vc_ancestor_calls, vc_ancestor_calls) end + if defined?(@__vc_content_is_a_slot) + child.instance_variable_set(:@__vc_content_is_a_slot, @__vc_content_is_a_slot) + end + super end @@ -687,6 +698,19 @@ def initialize_parameters def provided_collection_parameter @provided_collection_parameter ||= nil end + + def __vc_content_is_a_slot? + defined?(@__vc_content_is_a_slot) && @__vc_content_is_a_slot + end + + def content_is_a_slot! + @__vc_content_is_a_slot = true + renders_one :content + end + + def do_not_use_content_as_a_slot! + @__vc_content_is_a_slot = false + end end ActiveSupport.run_load_hooks(:view_component, self) diff --git a/lib/view_component/slotable.rb b/lib/view_component/slotable.rb index c537d4828..320d0a4ff 100644 --- a/lib/view_component/slotable.rb +++ b/lib/view_component/slotable.rb @@ -298,8 +298,10 @@ def define_slot(slot_name, collection:, callable:) end def validate_plural_slot_name(slot_name) - if RESERVED_NAMES[:plural].include?(slot_name.to_sym) - raise ReservedPluralSlotNameError.new(name, slot_name) + if slot_name.to_sym == :contents && !__vc_content_is_a_slot? + if RESERVED_NAMES[:plural].include?(slot_name.to_sym) + raise ReservedPluralSlotNameError.new(name, slot_name) + end end raise_if_slot_name_uncountable(slot_name) @@ -309,12 +311,12 @@ def validate_plural_slot_name(slot_name) end def validate_singular_slot_name(slot_name) - if slot_name.to_sym == :content + if slot_name.to_sym == :content && !__vc_content_is_a_slot? raise ContentSlotNameError.new(name) - end - - if RESERVED_NAMES[:singular].include?(slot_name.to_sym) - raise ReservedSingularSlotNameError.new(name, slot_name) + elsif !__vc_content_is_a_slot? + if RESERVED_NAMES[:singular].include?(slot_name.to_sym) + raise ReservedSingularSlotNameError.new(name, slot_name) + end end raise_if_slot_conflicts_with_call(slot_name) diff --git a/test/sandbox/app/components/content_as_slot_component.html.erb b/test/sandbox/app/components/content_as_slot_component.html.erb new file mode 100644 index 000000000..b784aa225 --- /dev/null +++ b/test/sandbox/app/components/content_as_slot_component.html.erb @@ -0,0 +1,5 @@ +
+ <% if content? %> + <%= content %> + <% end %> +
diff --git a/test/sandbox/app/components/content_as_slot_component.rb b/test/sandbox/app/components/content_as_slot_component.rb new file mode 100644 index 000000000..dead53d50 --- /dev/null +++ b/test/sandbox/app/components/content_as_slot_component.rb @@ -0,0 +1,3 @@ +class ContentAsSlotComponent < ViewComponent::Base + content_is_a_slot! +end diff --git a/test/sandbox/app/components/content_not_a_slot_component.rb b/test/sandbox/app/components/content_not_a_slot_component.rb new file mode 100644 index 000000000..1b3c5af1a --- /dev/null +++ b/test/sandbox/app/components/content_not_a_slot_component.rb @@ -0,0 +1,3 @@ +class ContentNotASlotComponent < ViewComponent::Base + do_not_use_content_as_a_slot! +end diff --git a/test/sandbox/test/slotable_test.rb b/test/sandbox/test/slotable_test.rb index 83f44a8ce..60b271f78 100644 --- a/test/sandbox/test/slotable_test.rb +++ b/test/sandbox/test/slotable_test.rb @@ -819,4 +819,62 @@ def test_overridden_slot_name_can_be_inherited 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_content_as_a_slot_component + component = ContentAsSlotComponent.new + render_inline(component) do |c| + c.with_content do + "The truth is out there" + end + end + + assert component.content? + assert_selector "div", text: "The truth is out there" + end + + def test_content_as_a_slot_component_with_content + component = ContentAsSlotComponent.new + component.with_content do + "The truth is out there" + end + render_inline(component) + + assert component.content? + assert_selector "div", text: "The truth is out there" + end + + def test_content_as_a_slot_inheritance + new_component_class = Class.new(ContentAsSlotComponent) + assert new_component_class.send(:__vc_content_is_a_slot?) + end + + def test_content_is_not_a_slot + new_component_class = Class.new(SlotsComponent) do + do_not_use_content_as_a_slot! + end + refute new_component_class.send(:__vc_content_is_a_slot?) + + render_inline(SlotsComponent.new) do |component| + component.with_title do + "This is my title!" + end + + component.with_subtitle do + "This is my subtitle!" + end + + component.with_footer do + "This is the footer" + end + end + + assert_text "No tabs provided" + assert_text "No items provided" + end + + def test_content_is_not_a_slot_inheritance + refute ContentNotASlotComponent.send(:__vc_content_is_a_slot?) + new_component_class = Class.new(ContentNotASlotComponent) + refute new_component_class.send(:__vc_content_is_a_slot?) + end end