Skip to content

Commit

Permalink
Got it working for variants and inline templates
Browse files Browse the repository at this point in the history
  • Loading branch information
camertron committed Jul 14, 2023
1 parent 9b81c19 commit 7991f70
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 16 deletions.
59 changes: 50 additions & 9 deletions lib/view_component/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/view_component/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= yield :foo %>
4 changes: 4 additions & 0 deletions test/sandbox/app/components/bad_yield_value_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class BadYieldValueComponent < ViewComponent::Base
end
2 changes: 1 addition & 1 deletion test/sandbox/app/components/level2_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div class="level2-component">
<div class="level2-component base">
<%= yield :parent %>
</div>
2 changes: 1 addition & 1 deletion test/sandbox/app/components/level3_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div class="level3-component">
<div class="level3-component base">
<%= yield :parent %>
</div>
39 changes: 39 additions & 0 deletions test/sandbox/test/inline_template_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def initialize(name)
class InlineErbSubclassComponent < InlineErbComponent
erb_template <<~ERB
<h1>Hey, <%= name %>!</h1>
<div class="parent">
<%= yield :parent %>
</div>
ERB
end

Expand Down Expand Up @@ -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
<div class="inline-template">
<%= yield :parent %>
</div>
ERB
end

test "renders inline templates" do
render_inline(InlineErbComponent.new("Fox Mulder"))

Expand Down Expand Up @@ -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
Expand Down
30 changes: 25 additions & 5 deletions test/sandbox/test/rendering_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7991f70

Please sign in to comment.