Skip to content

Commit 86b0149

Browse files
Use ActionView::TemplateDetails for handling format and variant (#2156)
* add .DS_Store to gitignore * Add Template subclasses to improve compiler polymorphism * Move template type-specific logic to constructors * Inline source into templates that require it * Flatten inline_call conditional in compile_to_component * Remove defined_on_self param from non-inline-call templates * Use ActionView logic for parsing template names Removes support for variant names containing `.`. * Delegate template format and variant to TemplateDetails --------- Co-authored-by: Joel Hawksley <[email protected]>
1 parent 0e226c4 commit 86b0149

File tree

7 files changed

+103
-88
lines changed

7 files changed

+103
-88
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
*.gem
22
*.rbc
33
.ruby-version
4+
.DS_Store
45
/.config
56
/coverage/assets
67
/coverage/index.html

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ nav_order: 5
2424

2525
*Joel Hawksley*
2626

27+
* BREAKING: Remove support for variant names containing `.` to be consistent with Rails.
28+
29+
*Stephen Nelson*
30+
2731
* Ensure HTML output safety wrapper is used for all inline templates.
2832

2933
*Joel Hawksley*

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ ViewComponent is built by over a hundred members of the community, including:
193193
<img src="https://avatars.githubusercontent.com/sammyhenningsson?s=64" alt="sammyhenningsson" width="32" />
194194
<img src="https://avatars.githubusercontent.com/sampart?s=64" alt="sampart" width="32" />
195195
<img src="https://avatars.githubusercontent.com/seanpdoyle?s=64" alt="seanpdoyle" width="32" />
196+
<img src="https://avatars.githubusercontent.com/sfnelson?s=64" alt="sfnelson" width="32" />
196197
<img src="https://avatars.githubusercontent.com/simonrand?s=64" alt="simonrand" width="32" />
197198
<img src="https://avatars.githubusercontent.com/skryukov?s=64" alt="skryukov" width="32" />
198199
<img src="https://avatars.githubusercontent.com/smashwilson?s=64" alt="smashwilson" width="32" />

lib/view_component/compiler.rb

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -169,27 +169,12 @@ def template_errors
169169
def gather_templates
170170
@templates ||=
171171
begin
172+
path_parser = ActionView::Resolver::PathParser.new
172173
templates = @component.sidecar_files(
173174
ActionView::Template.template_handler_extensions
174175
).map do |path|
175-
# Extract format and variant from template filename
176-
this_format, variant =
177-
File
178-
.basename(path) # "variants_component.html+mini.watch.erb"
179-
.split(".")[1..-2] # ["html+mini", "watch"]
180-
.join(".") # "html+mini.watch"
181-
.split("+") # ["html", "mini.watch"]
182-
.map(&:to_sym) # [:html, :"mini.watch"]
183-
184-
out = Template.new(
185-
component: @component,
186-
type: :file,
187-
path: path,
188-
lineno: 0,
189-
extension: path.split(".").last,
190-
this_format: this_format.to_s.split(".").last&.to_sym, # strip locale from this_format, see #2113
191-
variant: variant
192-
)
176+
details = path_parser.parse(path).details
177+
out = Template::File.new(component: @component, path: path, details: details)
193178

194179
out
195180
end
@@ -201,24 +186,17 @@ def gather_templates
201186
).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }
202187
.uniq
203188
.each do |method_name|
204-
templates << Template.new(
189+
templates << Template::InlineCall.new(
205190
component: @component,
206-
type: :inline_call,
207-
this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT,
208-
variant: method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil,
209191
method_name: method_name,
210192
defined_on_self: component_instance_methods_on_self.include?(method_name)
211193
)
212194
end
213195

214196
if @component.inline_template.present?
215-
templates << Template.new(
197+
templates << Template::Inline.new(
216198
component: @component,
217-
type: :inline,
218-
path: @component.inline_template.path,
219-
lineno: @component.inline_template.lineno,
220-
source: @component.inline_template.source.dup,
221-
extension: @component.inline_template.language
199+
inline_template: @component.inline_template
222200
)
223201
end
224202

lib/view_component/template.rb

Lines changed: 91 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,56 +5,112 @@ class Template
55
DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type, keyword_init: true)
66
DataNoSource = Struct.new(:source, :identifier, :type, keyword_init: true)
77

8-
attr_reader :variant, :this_format, :type
8+
attr_reader :details
9+
10+
delegate :format, :variant, to: :details
911

1012
def initialize(
1113
component:,
12-
type:,
13-
this_format: nil,
14-
variant: nil,
14+
details:,
1515
lineno: nil,
1616
path: nil,
17-
extension: nil,
18-
source: nil,
19-
method_name: nil,
20-
defined_on_self: true
17+
method_name: nil
2118
)
2219
@component = component
23-
@type = type
24-
@this_format = this_format
25-
@variant = variant&.to_sym
20+
@details = details
2621
@lineno = lineno
2722
@path = path
28-
@extension = extension
29-
@source = source
3023
@method_name = method_name
31-
@defined_on_self = defined_on_self
32-
33-
@source_originally_nil = @source.nil?
3424

3525
@call_method_name =
3626
if @method_name
3727
@method_name
3828
else
3929
out = +"call"
40-
out << "_#{normalized_variant_name}" if @variant.present?
41-
out << "_#{@this_format}" if @this_format.present? && @this_format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
30+
out << "_#{normalized_variant_name}" if variant.present?
31+
out << "_#{format}" if format.present? && format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
4232
out
4333
end
4434
end
4535

36+
class File < Template
37+
def initialize(component:, details:, path:)
38+
super(
39+
component: component,
40+
details: details,
41+
path: path,
42+
lineno: 0
43+
)
44+
end
45+
46+
def type
47+
:file
48+
end
49+
50+
# Load file each time we look up #source in case the file has been modified
51+
def source
52+
::File.read(@path)
53+
end
54+
end
55+
56+
class Inline < Template
57+
attr_reader :source
58+
59+
def initialize(component:, inline_template:)
60+
details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil)
61+
62+
super(
63+
component: component,
64+
details: details,
65+
path: inline_template.path,
66+
lineno: inline_template.lineno,
67+
)
68+
69+
@source = inline_template.source.dup
70+
end
71+
72+
def type
73+
:inline
74+
end
75+
end
76+
77+
class InlineCall < Template
78+
def initialize(component:, method_name:, defined_on_self:)
79+
variant = method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil
80+
details = ActionView::TemplateDetails.new(nil, nil, nil, variant)
81+
82+
super(
83+
component: component,
84+
details: details,
85+
method_name: method_name
86+
)
87+
88+
@defined_on_self = defined_on_self
89+
end
90+
91+
def type
92+
:inline_call
93+
end
94+
95+
def compile_to_component
96+
@component.define_method(safe_method_name, @component.instance_method(@call_method_name))
97+
end
98+
99+
def defined_on_self?
100+
@defined_on_self
101+
end
102+
end
103+
46104
def compile_to_component
47-
if !inline_call?
48-
@component.silence_redefinition_of_method(@call_method_name)
105+
@component.silence_redefinition_of_method(@call_method_name)
49106

50-
# rubocop:disable Style/EvalWithLocation
51-
@component.class_eval <<-RUBY, @path, @lineno
52-
def #{@call_method_name}
53-
#{compiled_source}
54-
end
55-
RUBY
56-
# rubocop:enable Style/EvalWithLocation
107+
# rubocop:disable Style/EvalWithLocation
108+
@component.class_eval <<-RUBY, @path, @lineno
109+
def #{@call_method_name}
110+
#{compiled_source}
57111
end
112+
RUBY
113+
# rubocop:enable Style/EvalWithLocation
58114

59115
@component.define_method(safe_method_name, @component.instance_method(@call_method_name))
60116
end
@@ -72,55 +128,39 @@ def requires_compiled_superclass?
72128
end
73129

74130
def inline_call?
75-
@type == :inline_call
131+
type == :inline_call
76132
end
77133

78134
def inline?
79-
@type == :inline
135+
type == :inline
80136
end
81137

82138
def default_format?
83-
@this_format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
84-
end
85-
86-
def format
87-
@this_format
139+
format.nil? || format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
88140
end
89141

90142
def safe_method_name
91143
"_#{@call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
92144
end
93145

94146
def normalized_variant_name
95-
@variant.to_s.gsub("-", "__").gsub(".", "___")
96-
end
97-
98-
def defined_on_self?
99-
@defined_on_self
147+
variant.to_s.gsub("-", "__")
100148
end
101149

102150
private
103151

104-
def source
105-
if @source_originally_nil
106-
# Load file each time we look up #source in case the file has been modified
107-
File.read(@path)
108-
else
109-
@source
110-
end
111-
end
112-
113152
def compiled_source
114-
handler = ActionView::Template.handler_for_extension(@extension)
153+
handler = details.handler_class
115154
this_source = source
116155
this_source.rstrip! if @component.strip_trailing_whitespace?
117156

118157
short_identifier = defined?(Rails.root) ? @path.sub("#{Rails.root}/", "") : @path
119-
type = ActionView::Template::Types[@this_format]
158+
format = self.format || ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
159+
type = ActionView::Template::Types[format]
120160

121161
if handler.method(:call).parameters.length > 1
122162
handler.call(
123-
DataWithSource.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type),
163+
DataWithSource.new(format: format, identifier: @path, short_identifier: short_identifier, type: type),
124164
this_source
125165
)
126166
# :nocov:

test/sandbox/app/components/variants_component.html+mini.watch.erb

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/sandbox/test/rendering_test.rb

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,6 @@ def test_renders_component_with_variant_containing_a_dash
198198
end
199199
end
200200

201-
def test_renders_component_with_variant_containing_a_dot
202-
with_variant :"mini.watch" do
203-
render_inline(VariantsComponent.new)
204-
205-
assert_text("Mini Watch with dot")
206-
end
207-
end
208-
209201
def test_renders_default_template_when_variant_template_is_not_present
210202
with_variant :variant_without_template do
211203
render_inline(VariantsComponent.new)

0 commit comments

Comments
 (0)