diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index d535cfa7e..ffdf9fd7a 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -43,15 +43,41 @@ def compile(raise_errors: false, force: false) component_class.validate_collection_parameter! end + unique_superclass_name = methodize(component_class.superclass.name) + if has_inline_template? template = component_class.inline_template + unique_method_name = "call__#{methodize(component_class.name)}" redefinition_lock.synchronize do component_class.silence_redefinition_of_method("call") # rubocop:disable Style/EvalWithLocation - component_class.class_eval <<-RUBY, template.path, template.lineno + component_class.class_eval <<-RUBY, template.path, template.lineno - 1 + private def #{unique_method_name} + if block_given? + #{compiled_inline_template(template)} + else + #{unique_method_name} do |msg| + case msg + when :parent + super_method_name = if @__vc_variant + super_variant_method_name = :"call_\#{@__vc_variant}__#{unique_superclass_name}" + respond_to?(super_variant_method_name, true) ? super_variant_method_name : nil + end + + super_method_name ||= :call__#{unique_superclass_name} + send(super_method_name) + + nil + else + raise UnexpectedTemplateYield.new(msg) + end + end + end + end + def call - #{compiled_inline_template(template)} + #{unique_method_name} end RUBY # rubocop:enable Style/EvalWithLocation @@ -75,17 +101,32 @@ def render_template_for(variant = nil) component_class.silence_redefinition_of_method(unique_method_name) # rubocop:disable Style/EvalWithLocation - component_class.class_eval <<-RUBY, template[:path], 0 + component_class.class_eval <<-RUBY, template[:path], -1 private def #{unique_method_name} - #{compiled_template(template[:path])} + if block_given? + #{compiled_template(template[:path])} + else + #{unique_method_name} do |msg| + case msg + when :parent + super_method_name = if @__vc_variant + super_variant_method_name = :"call_\#{@__vc_variant}__#{unique_superclass_name}" + respond_to?(super_variant_method_name, true) ? super_variant_method_name : nil + end + + super_method_name ||= :call__#{unique_superclass_name} + send(super_method_name) + + nil + else + raise UnexpectedTemplateYield.new(msg) + end + end + end end def #{method_name} - #{unique_method_name} do |msg| - if msg == :parent - capture { super } - end - end + #{unique_method_name} end RUBY # rubocop:enable Style/EvalWithLocation diff --git a/lib/view_component/errors.rb b/lib/view_component/errors.rb index 36f9ee570..49575c930 100644 --- a/lib/view_component/errors.rb +++ b/lib/view_component/errors.rb @@ -220,4 +220,12 @@ def initialize(setter_method_name, setter_name) super(MESSAGE.gsub("SETTER_METHOD_NAME", setter_method_name.to_s).gsub("SETTER_NAME", setter_name.to_s)) end end + + class UnexpectedTemplateYield < StandardError + MESSAGE = "An unexpected value 'YIELDED_VALUE' was yielded inside a component template. Only :parent is allowed." + + def initialize(yielded_value) + super(MESSAGE.gsub("YIELDED_VALUE", yielded_value.inspect)) + end + end end diff --git a/test/sandbox/app/components/bad_yield_value_component.html.erb b/test/sandbox/app/components/bad_yield_value_component.html.erb new file mode 100644 index 000000000..fdeb28d3a --- /dev/null +++ b/test/sandbox/app/components/bad_yield_value_component.html.erb @@ -0,0 +1 @@ +<%= yield :foo %> diff --git a/test/sandbox/app/components/bad_yield_value_component.rb b/test/sandbox/app/components/bad_yield_value_component.rb new file mode 100644 index 000000000..fe2144221 --- /dev/null +++ b/test/sandbox/app/components/bad_yield_value_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class BadYieldValueComponent < ViewComponent::Base +end diff --git a/test/sandbox/app/components/level2_component.html.erb b/test/sandbox/app/components/level2_component.html.erb index 86af3fe9a..2a7a4c490 100644 --- a/test/sandbox/app/components/level2_component.html.erb +++ b/test/sandbox/app/components/level2_component.html.erb @@ -1,3 +1,3 @@ -
+
<%= yield :parent %>
diff --git a/test/sandbox/app/components/level3_component.html.erb b/test/sandbox/app/components/level3_component.html.erb index 90c9c8d29..695db1105 100644 --- a/test/sandbox/app/components/level3_component.html.erb +++ b/test/sandbox/app/components/level3_component.html.erb @@ -1,3 +1,3 @@ -
+
<%= yield :parent %>
diff --git a/test/sandbox/test/inline_template_test.rb b/test/sandbox/test/inline_template_test.rb index 76ce0877b..53366411b 100644 --- a/test/sandbox/test/inline_template_test.rb +++ b/test/sandbox/test/inline_template_test.rb @@ -30,6 +30,9 @@ def initialize(name) class InlineErbSubclassComponent < InlineErbComponent erb_template <<~ERB

Hey, <%= name %>!

+
+ <%= yield :parent %> +
ERB end @@ -76,6 +79,20 @@ def initialize(name) end end + class InlineBadYieldComponent < ViewComponent::Base + erb_template <<~ERB + <%= yield :foo %> + ERB + end + + class InlineComponentDerivedFromComponentSupportingVariants < Level2Component + erb_template <<~ERB +
+ <%= yield :parent %> +
+ ERB + end + test "renders inline templates" do render_inline(InlineErbComponent.new("Fox Mulder")) @@ -112,6 +129,28 @@ def initialize(name) assert_selector("h1", text: "Hey, Fox Mulder!") end + test "child components can render their parent" do + render_inline(InlineErbSubclassComponent.new("Fox Mulder")) + + assert_selector(".parent h1", text: "Hello, Fox Mulder!") + end + + test "inline child component propagates variant to parent" do + with_variant :variant do + render_inline(InlineComponentDerivedFromComponentSupportingVariants.new) + end + + assert_selector ".inline-template .level2-component.variant .level1-component" + end + + test "yielding unexpected value raises error" do + error = assert_raises(ViewComponent::UnexpectedTemplateYield) do + render_inline(InlineBadYieldComponent.new) + end + + assert_equal "An unexpected value ':foo' was yielded inside a component template. Only :parent is allowed.", error.message + end + test "calling template methods multiple times raises an exception" do error = assert_raises ViewComponent::MultipleInlineTemplatesError do Class.new(InlineErbComponent) do diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb index bb2a29731..96bdb5b02 100644 --- a/test/sandbox/test/rendering_test.rb +++ b/test/sandbox/test/rendering_test.rb @@ -960,14 +960,34 @@ def test_inherited_component_renders_when_lazy_loading assert_selector("div", text: "hello, my own template") end - def test_inherited_component_calls_super + def test_child_components_can_render_parent render_inline(Level3Component.new) - assert_selector(".level3-component", count: 1) do |level3| - level3.assert_selector(".level2-component", count: 1) do |level2| - level2.assert_selector(".level1-component", count: 1) - end + assert_selector(".level3-component.base .level2-component.base .level1-component") + end + + def test_variant_propagates_to_parent + with_variant :variant do + render_inline(Level3Component.new) + end + + assert_selector ".level3-component.variant .level2-component.variant .level1-component" + end + + def test_child_components_fall_back_to_default_variant + with_variant :non_existent_variant do + render_inline(Level3Component.new) + end + + assert_selector ".level3-component.base .level2-component.base .level1-component" + end + + def test_yielding_unexpected_value_raises_error + error = assert_raises(ViewComponent::UnexpectedTemplateYield) do + render_inline(BadYieldValueComponent.new) end + + assert_equal "An unexpected value ':foo' was yielded inside a component template. Only :parent is allowed.", error.message end def test_component_renders_without_trailing_whitespace