From 314a9ea2023a407df4dc81cbd0c1b6cbe2c3e2a5 Mon Sep 17 00:00:00 2001 From: David Uhlig Date: Tue, 14 Jan 2025 21:53:02 +0100 Subject: [PATCH 1/3] Expand ViewComponent detection logic Enhance the discovery process to identify ViewComponents by name, even when they don't explicitly end with "Component". This update ensures compatibility with Theo expressions that omit "Component". --- lib/theo-rails/theo.rb | 32 +++++++++++++++----------------- spec/theo-rails/theo_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/lib/theo-rails/theo.rb b/lib/theo-rails/theo.rb index 35ba258..f2457cb 100644 --- a/lib/theo-rails/theo.rb +++ b/lib/theo-rails/theo.rb @@ -46,10 +46,18 @@ 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) - if is_partial + if component.present? + if content + output = "<%= render #{component}.new(#{locals}) do#{yields} %>#{process(content)}<% end %>" + elsif collection + locals = ", #{locals}" unless locals.empty? + output = "<%= render #{component}.with_collection(#{collection}#{locals}) %>" + else + output = "<%= render #{component}.new(#{locals}) %>" + end + else partial = partial.delete_prefix('_').underscore partial = "#{path}/#{partial}" if path @@ -64,17 +72,6 @@ def process(source) locals = ", locals: {#{locals}}" unless locals.empty? 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 - locals = ", #{locals}" unless locals.empty? - output = "<%= render #{component}.with_collection(#{collection}#{locals}) %>" - else - output = "<%= render #{component}.new(#{locals}) %>" - end end output @@ -107,11 +104,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 CamelCase + klass = component.safe_constantize || "#{component}Component".safe_constantize + klass.to_s if 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', + %(