diff --git a/lib/theo-rails/theo.rb b/lib/theo-rails/theo.rb
index 35ba258..f3cc877 100644
--- a/lib/theo-rails/theo.rb
+++ b/lib/theo-rails/theo.rb
@@ -46,8 +46,8 @@ def process(source)
locals = attributes.empty? ? '' : attributes.map { |k, v| "'#{k}': #{v}" }.join(', ')
- is_component = view_component_exists?(partial)
- is_partial = !is_component
+ component = resolve_view_component(partial)
+ is_partial = component.nil?
if is_partial
partial = partial.delete_prefix('_').underscore
@@ -65,8 +65,6 @@ def process(source)
output = "<%= render partial: '#{partial}'#{collection}#{locals} %>"
end
else
- component = "#{partial}Component"
-
if content
output = "<%= render #{component}.new(#{locals}) do#{yields} %>#{process(content)}<% end %>"
elsif collection
@@ -107,11 +105,12 @@ def view_component_loaded?
@view_component_loaded ||= Object.const_defined?('ViewComponent')
end
- def view_component_exists?(component)
+ def resolve_view_component(component)
return unless view_component_loaded?
- is_capitalized = /^[A-Z]/.match?(component)
- is_capitalized && Object.const_defined?("#{component}Component")
+ # safe_constantize ensures PascalCase
+ klass = component.safe_constantize || "#{component}Component".safe_constantize
+ klass.name if klass && klass < ViewComponent::Base
end
def translate_location(spot, backtrace_location, source)
diff --git a/spec/theo-rails/theo_spec.rb b/spec/theo-rails/theo_spec.rb
index 208d5e9..5062a71 100644
--- a/spec/theo-rails/theo_spec.rb
+++ b/spec/theo-rails/theo_spec.rb
@@ -3,6 +3,15 @@
class WidgetComponent < ViewComponent::Base
end
+class Button < ViewComponent::Base
+end
+
+class Avatar < ViewComponent::Base
+end
+
+class AvatarComponent < ViewComponent::Base
+end
+
RSpec.shared_examples 'theo to erb' do |name, input, output|
let(:theo) { Theo::Rails::Theo.new }
@@ -146,6 +155,22 @@ class WidgetComponent < ViewComponent::Base
%(),
%(<%= render WidgetComponent.with_collection(widgets, 'attr1': 'value1', 'attr2': 'value2') %>)
end
+
+ context 'component without "Component" suffix' do
+ include_examples 'theo to erb', 'evaluates simple component',
+ %(),
+ %(<%= render Button.new() %>)
+ end
+
+ context 'competing component names' do
+ include_examples 'theo to erb', 'evaluates direct match',
+ %(),
+ %(<%= render Avatar.new() %>)
+
+ include_examples 'theo to erb', 'evaluates direct match with "Component" suffix',
+ %(),
+ %(<%= render AvatarComponent.new() %>)
+ end
end
context 'erb compatibility' do