view_component 2.18.2 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +127 -79
- data/README.md +5 -1023
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component.rb +1 -0
- data/lib/view_component/base.rb +22 -192
- data/lib/view_component/collection.rb +2 -0
- data/lib/view_component/compiler.rb +214 -0
- data/lib/view_component/engine.rb +10 -5
- data/lib/view_component/preview.rb +4 -1
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/version.rb +2 -2
- metadata +7 -6
@@ -1,7 +1,7 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class <%= class_name %>ComponentTest < ViewComponent::TestCase
|
4
|
-
|
4
|
+
def test_component_renders_something_useful
|
5
5
|
# assert_equal(
|
6
6
|
# %(<span>Hello, components!</span>),
|
7
7
|
# render_inline(<%= class_name %>Component.new(message: "Hello, components!")).css("span").to_html
|
data/lib/view_component.rb
CHANGED
data/lib/view_component/base.rb
CHANGED
@@ -64,7 +64,7 @@ module ViewComponent
|
|
64
64
|
@virtual_path ||= virtual_path
|
65
65
|
|
66
66
|
# For template variants (+phone, +desktop, etc.)
|
67
|
-
@variant
|
67
|
+
@variant ||= @lookup_context.variants.first
|
68
68
|
|
69
69
|
# For caching, such as #cache_if
|
70
70
|
@current_template = nil unless defined?(@current_template)
|
@@ -77,7 +77,7 @@ module ViewComponent
|
|
77
77
|
before_render
|
78
78
|
|
79
79
|
if render?
|
80
|
-
|
80
|
+
render_template_for(@variant)
|
81
81
|
else
|
82
82
|
""
|
83
83
|
end
|
@@ -102,11 +102,7 @@ module ViewComponent
|
|
102
102
|
# If trying to render a partial or template inside a component,
|
103
103
|
# pass the render call to the parent view_context.
|
104
104
|
def render(options = {}, args = {}, &block)
|
105
|
-
|
106
|
-
view_context.render(options, args, &block)
|
107
|
-
else
|
108
|
-
super
|
109
|
-
end
|
105
|
+
view_context.render(options, args, &block)
|
110
106
|
end
|
111
107
|
|
112
108
|
def controller
|
@@ -117,7 +113,7 @@ module ViewComponent
|
|
117
113
|
# Provides a proxy to access helper methods from the context of the current controller
|
118
114
|
def helpers
|
119
115
|
raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
|
120
|
-
@helpers ||=
|
116
|
+
@helpers ||= view_context
|
121
117
|
end
|
122
118
|
|
123
119
|
# Exposes .virutal_path as an instance method
|
@@ -149,6 +145,12 @@ module ViewComponent
|
|
149
145
|
nil
|
150
146
|
end
|
151
147
|
|
148
|
+
def with_variant(variant)
|
149
|
+
@variant = variant
|
150
|
+
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
152
154
|
private
|
153
155
|
|
154
156
|
# Exposes the current request to the component.
|
@@ -183,8 +185,13 @@ module ViewComponent
|
|
183
185
|
end
|
184
186
|
|
185
187
|
def inherited(child)
|
186
|
-
#
|
187
|
-
|
188
|
+
# Compile so child will inherit compiled `call_*` template methods that
|
189
|
+
# `compile` defines
|
190
|
+
compile
|
191
|
+
|
192
|
+
# If Rails application is loaded, add application url_helpers to the component context
|
193
|
+
# we need to check this to use this gem as a dependency
|
194
|
+
if defined?(Rails) && Rails.application
|
188
195
|
child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
|
189
196
|
end
|
190
197
|
|
@@ -203,16 +210,8 @@ module ViewComponent
|
|
203
210
|
super
|
204
211
|
end
|
205
212
|
|
206
|
-
def call_method_name(variant)
|
207
|
-
if variant.present? && variants.include?(variant)
|
208
|
-
"call_#{variant}"
|
209
|
-
else
|
210
|
-
"call"
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
213
|
def compiled?
|
215
|
-
|
214
|
+
template_compiler.compiled?
|
216
215
|
end
|
217
216
|
|
218
217
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
@@ -220,75 +219,11 @@ module ViewComponent
|
|
220
219
|
# Do as much work as possible in this step, as doing so reduces the amount
|
221
220
|
# of work done each time a component is rendered.
|
222
221
|
def compile(raise_errors: false)
|
223
|
-
|
224
|
-
|
225
|
-
if template_errors.present?
|
226
|
-
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
227
|
-
return false
|
228
|
-
end
|
229
|
-
|
230
|
-
if instance_methods(false).include?(:before_render_check)
|
231
|
-
ActiveSupport::Deprecation.warn(
|
232
|
-
"`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
|
233
|
-
)
|
234
|
-
end
|
235
|
-
|
236
|
-
# Remove any existing singleton methods,
|
237
|
-
# as Ruby warns when redefining a method.
|
238
|
-
remove_possible_singleton_method(:variants)
|
239
|
-
remove_possible_singleton_method(:collection_parameter)
|
240
|
-
remove_possible_singleton_method(:collection_counter_parameter)
|
241
|
-
remove_possible_singleton_method(:counter_argument_present?)
|
242
|
-
|
243
|
-
define_singleton_method(:variants) do
|
244
|
-
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
245
|
-
end
|
246
|
-
|
247
|
-
define_singleton_method(:collection_parameter) do
|
248
|
-
if provided_collection_parameter
|
249
|
-
provided_collection_parameter
|
250
|
-
else
|
251
|
-
name.demodulize.underscore.chomp("_component").to_sym
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
define_singleton_method(:collection_counter_parameter) do
|
256
|
-
"#{collection_parameter}_counter".to_sym
|
257
|
-
end
|
258
|
-
|
259
|
-
define_singleton_method(:counter_argument_present?) do
|
260
|
-
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
|
261
|
-
end
|
262
|
-
|
263
|
-
validate_collection_parameter! if raise_errors
|
264
|
-
|
265
|
-
# If template name annotations are turned on, a line is dynamically
|
266
|
-
# added with a comment. In this case, we want to return a different
|
267
|
-
# starting line number so errors that are raised will point to the
|
268
|
-
# correct line in the component template.
|
269
|
-
line_number =
|
270
|
-
if ActionView::Base.respond_to?(:annotate_rendered_view_with_filenames) &&
|
271
|
-
ActionView::Base.annotate_rendered_view_with_filenames
|
272
|
-
-2
|
273
|
-
else
|
274
|
-
-1
|
275
|
-
end
|
276
|
-
|
277
|
-
templates.each do |template|
|
278
|
-
# Remove existing compiled template methods,
|
279
|
-
# as Ruby warns when redefining a method.
|
280
|
-
method_name = call_method_name(template[:variant])
|
281
|
-
undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)
|
282
|
-
|
283
|
-
class_eval <<-RUBY, template[:path], line_number
|
284
|
-
def #{method_name}
|
285
|
-
@output_buffer = ActionView::OutputBuffer.new
|
286
|
-
#{compiled_template(template[:path])}
|
287
|
-
end
|
288
|
-
RUBY
|
289
|
-
end
|
222
|
+
template_compiler.compile(raise_errors: raise_errors)
|
223
|
+
end
|
290
224
|
|
291
|
-
|
225
|
+
def template_compiler
|
226
|
+
@_template_compiler ||= Compiler.new(self)
|
292
227
|
end
|
293
228
|
|
294
229
|
# we'll eventually want to update this to support other types
|
@@ -353,111 +288,6 @@ module ViewComponent
|
|
353
288
|
@provided_collection_parameter ||= nil
|
354
289
|
end
|
355
290
|
|
356
|
-
def compiled_template(file_path)
|
357
|
-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
358
|
-
template = File.read(file_path)
|
359
|
-
|
360
|
-
if handler.method(:call).parameters.length > 1
|
361
|
-
handler.call(self, template)
|
362
|
-
else
|
363
|
-
handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
|
364
|
-
end
|
365
|
-
end
|
366
|
-
|
367
|
-
def inline_calls
|
368
|
-
@inline_calls ||=
|
369
|
-
begin
|
370
|
-
# Fetch only ViewComponent ancestor classes to limit the scope of
|
371
|
-
# finding inline calls
|
372
|
-
view_component_ancestors =
|
373
|
-
ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - included_modules
|
374
|
-
|
375
|
-
view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
|
376
|
-
end
|
377
|
-
end
|
378
|
-
|
379
|
-
def inline_calls_defined_on_self
|
380
|
-
@inline_calls_defined_on_self ||= instance_methods(false).grep(/^call/)
|
381
|
-
end
|
382
|
-
|
383
|
-
def matching_views_in_source_location
|
384
|
-
return [] unless source_location
|
385
|
-
|
386
|
-
location_without_extension = source_location.chomp(File.extname(source_location))
|
387
|
-
|
388
|
-
extensions = ActionView::Template.template_handler_extensions.join(",")
|
389
|
-
|
390
|
-
# view files in the same directory as the component
|
391
|
-
sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
|
392
|
-
|
393
|
-
# view files in a directory named like the component
|
394
|
-
directory = File.dirname(source_location)
|
395
|
-
filename = File.basename(source_location, ".rb")
|
396
|
-
component_name = name.demodulize.underscore
|
397
|
-
|
398
|
-
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
|
399
|
-
|
400
|
-
(sidecar_files - [source_location] + sidecar_directory_files)
|
401
|
-
end
|
402
|
-
|
403
|
-
def templates
|
404
|
-
@templates ||=
|
405
|
-
matching_views_in_source_location.each_with_object([]) do |path, memo|
|
406
|
-
pieces = File.basename(path).split(".")
|
407
|
-
|
408
|
-
memo << {
|
409
|
-
path: path,
|
410
|
-
variant: pieces.second.split("+").second&.to_sym,
|
411
|
-
handler: pieces.last
|
412
|
-
}
|
413
|
-
end
|
414
|
-
end
|
415
|
-
|
416
|
-
def template_errors
|
417
|
-
@template_errors ||=
|
418
|
-
begin
|
419
|
-
errors = []
|
420
|
-
|
421
|
-
if (templates + inline_calls).empty?
|
422
|
-
errors << "Could not find a template file or inline render method for #{self}."
|
423
|
-
end
|
424
|
-
|
425
|
-
if templates.count { |template| template[:variant].nil? } > 1
|
426
|
-
errors << "More than one template found for #{self}. There can only be one default template file per component."
|
427
|
-
end
|
428
|
-
|
429
|
-
invalid_variants = templates
|
430
|
-
.group_by { |template| template[:variant] }
|
431
|
-
.map { |variant, grouped| variant if grouped.length > 1 }
|
432
|
-
.compact
|
433
|
-
.sort
|
434
|
-
|
435
|
-
unless invalid_variants.empty?
|
436
|
-
errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be one template file per variant."
|
437
|
-
end
|
438
|
-
|
439
|
-
if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
|
440
|
-
errors << "Template file and inline render method found for #{self}. There can only be a template file or inline render method per component."
|
441
|
-
end
|
442
|
-
|
443
|
-
duplicate_template_file_and_inline_variant_calls =
|
444
|
-
templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
|
445
|
-
|
446
|
-
unless duplicate_template_file_and_inline_variant_calls.empty?
|
447
|
-
count = duplicate_template_file_and_inline_variant_calls.count
|
448
|
-
|
449
|
-
errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be a template file or inline render method per variant."
|
450
|
-
end
|
451
|
-
|
452
|
-
errors
|
453
|
-
end
|
454
|
-
end
|
455
|
-
|
456
|
-
def variants_from_inline_calls(calls)
|
457
|
-
calls.reject { |call| call == :call }.map do |variant_call|
|
458
|
-
variant_call.to_s.sub("call_", "").to_sym
|
459
|
-
end
|
460
|
-
end
|
461
291
|
end
|
462
292
|
|
463
293
|
ActiveSupport.run_load_hooks(:view_component, self)
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
class Compiler
|
5
|
+
def initialize(component_class)
|
6
|
+
@component_class = component_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def compiled?
|
10
|
+
CompileCache.compiled?(component_class)
|
11
|
+
end
|
12
|
+
|
13
|
+
def compile(raise_errors: false)
|
14
|
+
return if compiled?
|
15
|
+
|
16
|
+
if template_errors.present?
|
17
|
+
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
if component_class.instance_methods(false).include?(:before_render_check)
|
22
|
+
ActiveSupport::Deprecation.warn(
|
23
|
+
"`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Remove any existing singleton methods,
|
28
|
+
# as Ruby warns when redefining a method.
|
29
|
+
component_class.remove_possible_singleton_method(:collection_parameter)
|
30
|
+
component_class.remove_possible_singleton_method(:collection_counter_parameter)
|
31
|
+
component_class.remove_possible_singleton_method(:counter_argument_present?)
|
32
|
+
|
33
|
+
component_class.define_singleton_method(:collection_parameter) do
|
34
|
+
if provided_collection_parameter
|
35
|
+
provided_collection_parameter
|
36
|
+
else
|
37
|
+
name.demodulize.underscore.chomp("_component").to_sym
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
component_class.define_singleton_method(:collection_counter_parameter) do
|
42
|
+
"#{collection_parameter}_counter".to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
component_class.define_singleton_method(:counter_argument_present?) do
|
46
|
+
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
|
47
|
+
end
|
48
|
+
|
49
|
+
component_class.validate_collection_parameter! if raise_errors
|
50
|
+
|
51
|
+
templates.each do |template|
|
52
|
+
# Remove existing compiled template methods,
|
53
|
+
# as Ruby warns when redefining a method.
|
54
|
+
method_name = call_method_name(template[:variant])
|
55
|
+
component_class.send(:undef_method, method_name.to_sym) if component_class.instance_methods.include?(method_name.to_sym)
|
56
|
+
|
57
|
+
component_class.class_eval <<-RUBY, template[:path], -1
|
58
|
+
def #{method_name}
|
59
|
+
@output_buffer = ActionView::OutputBuffer.new
|
60
|
+
#{compiled_template(template[:path])}
|
61
|
+
end
|
62
|
+
RUBY
|
63
|
+
end
|
64
|
+
|
65
|
+
define_render_template_for
|
66
|
+
|
67
|
+
CompileCache.register(component_class)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
attr_reader :component_class
|
73
|
+
|
74
|
+
def define_render_template_for
|
75
|
+
component_class.send(:undef_method, :render_template_for) if component_class.instance_methods.include?(:render_template_for)
|
76
|
+
|
77
|
+
variant_elsifs = variants.compact.uniq.map do |variant|
|
78
|
+
"elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
|
79
|
+
end.join("\n")
|
80
|
+
|
81
|
+
component_class.class_eval <<-RUBY
|
82
|
+
def render_template_for(variant = nil)
|
83
|
+
if variant.nil?
|
84
|
+
call
|
85
|
+
#{variant_elsifs}
|
86
|
+
else
|
87
|
+
call
|
88
|
+
end
|
89
|
+
end
|
90
|
+
RUBY
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
def template_errors
|
95
|
+
@_template_errors ||= begin
|
96
|
+
errors = []
|
97
|
+
|
98
|
+
if (templates + inline_calls).empty?
|
99
|
+
errors << "Could not find a template file or inline render method for #{component_class}."
|
100
|
+
end
|
101
|
+
|
102
|
+
if templates.count { |template| template[:variant].nil? } > 1
|
103
|
+
errors << "More than one template found for #{component_class}. There can only be one default template file per component."
|
104
|
+
end
|
105
|
+
|
106
|
+
invalid_variants = templates
|
107
|
+
.group_by { |template| template[:variant] }
|
108
|
+
.map { |variant, grouped| variant if grouped.length > 1 }
|
109
|
+
.compact
|
110
|
+
.sort
|
111
|
+
|
112
|
+
unless invalid_variants.empty?
|
113
|
+
errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be one template file per variant."
|
114
|
+
end
|
115
|
+
|
116
|
+
if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
|
117
|
+
errors << "Template file and inline render method found for #{component_class}. There can only be a template file or inline render method per component."
|
118
|
+
end
|
119
|
+
|
120
|
+
duplicate_template_file_and_inline_variant_calls =
|
121
|
+
templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
|
122
|
+
|
123
|
+
unless duplicate_template_file_and_inline_variant_calls.empty?
|
124
|
+
count = duplicate_template_file_and_inline_variant_calls.count
|
125
|
+
|
126
|
+
errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be a template file or inline render method per variant."
|
127
|
+
end
|
128
|
+
|
129
|
+
errors
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def templates
|
134
|
+
@templates ||= matching_views_in_source_location.each_with_object([]) do |path, memo|
|
135
|
+
pieces = File.basename(path).split(".")
|
136
|
+
|
137
|
+
memo << {
|
138
|
+
path: path,
|
139
|
+
variant: pieces.second.split("+").second&.to_sym,
|
140
|
+
handler: pieces.last
|
141
|
+
}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def matching_views_in_source_location
|
146
|
+
source_location = component_class.source_location
|
147
|
+
return [] unless source_location
|
148
|
+
|
149
|
+
location_without_extension = source_location.chomp(File.extname(source_location))
|
150
|
+
|
151
|
+
extensions = ActionView::Template.template_handler_extensions.join(",")
|
152
|
+
|
153
|
+
# view files in the same directory as the component
|
154
|
+
sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
|
155
|
+
|
156
|
+
# view files in a directory named like the component
|
157
|
+
directory = File.dirname(source_location)
|
158
|
+
filename = File.basename(source_location, ".rb")
|
159
|
+
component_name = component_class.name.demodulize.underscore
|
160
|
+
|
161
|
+
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
|
162
|
+
|
163
|
+
(sidecar_files - [source_location] + sidecar_directory_files)
|
164
|
+
end
|
165
|
+
|
166
|
+
def inline_calls
|
167
|
+
@inline_calls ||= begin
|
168
|
+
# Fetch only ViewComponent ancestor classes to limit the scope of
|
169
|
+
# finding inline calls
|
170
|
+
view_component_ancestors =
|
171
|
+
component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - component_class.included_modules
|
172
|
+
|
173
|
+
view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def inline_calls_defined_on_self
|
178
|
+
@inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call/)
|
179
|
+
end
|
180
|
+
|
181
|
+
def variants
|
182
|
+
@_variants = (
|
183
|
+
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
184
|
+
).compact.uniq
|
185
|
+
end
|
186
|
+
|
187
|
+
def variants_from_inline_calls(calls)
|
188
|
+
calls.reject { |call| call == :call }.map do |variant_call|
|
189
|
+
variant_call.to_s.sub("call_", "").to_sym
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# :nocov:
|
194
|
+
def compiled_template(file_path)
|
195
|
+
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
196
|
+
template = File.read(file_path)
|
197
|
+
|
198
|
+
if handler.method(:call).parameters.length > 1
|
199
|
+
handler.call(component_class, template)
|
200
|
+
else
|
201
|
+
handler.call(OpenStruct.new(source: template, identifier: component_class.identifier, type: component_class.type))
|
202
|
+
end
|
203
|
+
end
|
204
|
+
# :nocov:
|
205
|
+
|
206
|
+
def call_method_name(variant)
|
207
|
+
if variant.present? && variants.include?(variant)
|
208
|
+
"call_#{variant}"
|
209
|
+
else
|
210
|
+
"call"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|