From d514f5d9525461764fb8815bf2344e74aec6a0f9 Mon Sep 17 00:00:00 2001 From: Andrew H Schwartz Date: Tue, 27 Aug 2024 12:29:32 -0400 Subject: [PATCH] Add slot methods to a module instead of the component class itself (#2040) * Add distinct GeneratedSlotMethods module to ViewComponents and define `#{slot}` and `#{slot?}` methods on this module, allowing these methods to be overridden with access to `super` implementation. * Update docs/CONTRIBUTING.md * Update docs/CHANGELOG.md --------- Co-authored-by: Joel Hawksley Co-authored-by: Joel Hawksley --- docs/CHANGELOG.md | 4 +++ docs/index.md | 1 + lib/view_component/slotable.rb | 25 +++++++++----- .../slot_name_override_component.html.erb | 7 ++++ .../slot_name_override_component.rb | 27 +++++++++++++++ test/sandbox/test/slotable_test.rb | 34 +++++++++++++++++++ 6 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 test/sandbox/app/components/slot_name_override_component.html.erb create mode 100644 test/sandbox/app/components/slot_name_override_component.rb diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 10da607f6..3f3411017 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,10 @@ nav_order: 5 ## main +* Allow overridden slot methods to use `super`. + + *Andrew Schwartz* + * Add Rails engine support to generators. *Tomasz Kowalewski* diff --git a/docs/index.md b/docs/index.md index eabd84dad..7ee5b460b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -182,6 +182,7 @@ ViewComponent is built by over a hundred members of the community, including: nicolas-brousse nshki nshki +ozydingo patrickarnett rainerborene rdavid1099 diff --git a/lib/view_component/slotable.rb b/lib/view_component/slotable.rb index c4c8bc29d..c537d4828 100644 --- a/lib/view_component/slotable.rb +++ b/lib/view_component/slotable.rb @@ -89,11 +89,11 @@ def renders_one(slot_name, callable = nil) end ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true) - define_method slot_name do + self::GeneratedSlotMethods.define_method slot_name do get_slot(slot_name) end - define_method :"#{slot_name}?" do + self::GeneratedSlotMethods.define_method :"#{slot_name}?" do get_slot(slot_name).present? end @@ -176,11 +176,11 @@ def renders_many(slot_name, callable = nil) end end - define_method slot_name do + self::GeneratedSlotMethods.define_method slot_name do get_slot(slot_name) end - define_method :"#{slot_name}?" do + self::GeneratedSlotMethods.define_method :"#{slot_name}?" do get_slot(slot_name).present? end @@ -199,19 +199,28 @@ def slot_type(slot_name) end end - # Clone slot configuration into child class - # see #test_slots_pollution def inherited(child) + # Clone slot configuration into child class + # see #test_slots_pollution child.registered_slots = registered_slots.clone + + # Add a module for slot methods, allowing them to be overriden by the component class + # see #test_slot_name_can_be_overriden + unless child.const_defined?(:GeneratedSlotMethods, false) + generated_slot_methods = Module.new + child.const_set(:GeneratedSlotMethods, generated_slot_methods) + child.include generated_slot_methods + end + super end def register_polymorphic_slot(slot_name, types, collection:) - define_method(slot_name) do + self::GeneratedSlotMethods.define_method(slot_name) do get_slot(slot_name) end - define_method(:"#{slot_name}?") do + self::GeneratedSlotMethods.define_method(:"#{slot_name}?") do get_slot(slot_name).present? end diff --git a/test/sandbox/app/components/slot_name_override_component.html.erb b/test/sandbox/app/components/slot_name_override_component.html.erb new file mode 100644 index 000000000..46b603473 --- /dev/null +++ b/test/sandbox/app/components/slot_name_override_component.html.erb @@ -0,0 +1,7 @@ +
+ <% if title? %> +
+ <%= title %> +
+ <% end %> +
diff --git a/test/sandbox/app/components/slot_name_override_component.rb b/test/sandbox/app/components/slot_name_override_component.rb new file mode 100644 index 000000000..8a89370ab --- /dev/null +++ b/test/sandbox/app/components/slot_name_override_component.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class SlotNameOverrideComponent < ViewComponent::Base + renders_one :title + + def initialize(title: nil) + @title = title + end + + def title + @title || super + end + + def title? + @title.present? || super + end +end + +class SlotNameOverrideComponent::OtherComponent < ViewComponent::Base + renders_one :title +end + +class SlotNameOverrideComponent::SubComponent < SlotNameOverrideComponent + def title + super.upcase + end +end diff --git a/test/sandbox/test/slotable_test.rb b/test/sandbox/test/slotable_test.rb index f638b0969..83f44a8ce 100644 --- a/test/sandbox/test/slotable_test.rb +++ b/test/sandbox/test/slotable_test.rb @@ -785,4 +785,38 @@ def test_slotable_default_instance assert_text "hello,world!", count: 1 end + + def test_slot_name_can_be_overriden + # Uses overridden `title` slot method + render_inline(SlotNameOverrideComponent.new(title: "Simple Title")) + + assert_selector(".title", text: "Simple Title") + end + + def test_slot_name_override_can_use_super + # Uses standard `title` slot method via `super` + render_inline(SlotNameOverrideComponent.new) do |component| + component.with_title do + "Block Title with More Complexity" + end + end + + assert_selector(".title", text: "Block Title with More Complexity") + end + + def overriden_slot_name_predicate_returns_false_when_not_set + render_inline(SlotNameOverrideComponent.new) + + refute_selector(".title") + end + + def test_overridden_slot_name_can_be_inherited + render_inline(SlotNameOverrideComponent::SubComponent.new(title: "lowercase")) + + assert_selector(".title", text: "LOWERCASE") + end + + 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 end