view_component 2.19.0 → 2.22.1

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.

@@ -1,7 +1,7 @@
1
1
  require "test_helper"
2
2
 
3
3
  class <%= class_name %>ComponentTest < ViewComponent::TestCase
4
- test "component renders something useful" do
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
@@ -6,6 +6,7 @@ module ViewComponent
6
6
  extend ActiveSupport::Autoload
7
7
 
8
8
  autoload :Base
9
+ autoload :Compiler
9
10
  autoload :Preview
10
11
  autoload :PreviewTemplateError
11
12
  autoload :TestHelpers
@@ -64,7 +64,7 @@ module ViewComponent
64
64
  @virtual_path ||= virtual_path
65
65
 
66
66
  # For template variants (+phone, +desktop, etc.)
67
- @variant = @lookup_context.variants.first
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
- send(self.class.call_method_name(@variant))
80
+ render_template_for(@variant)
81
81
  else
82
82
  ""
83
83
  end
@@ -149,6 +149,12 @@ module ViewComponent
149
149
  nil
150
150
  end
151
151
 
152
+ def with_variant(variant)
153
+ @variant = variant
154
+
155
+ self
156
+ end
157
+
152
158
  private
153
159
 
154
160
  # Exposes the current request to the component.
@@ -187,8 +193,9 @@ module ViewComponent
187
193
  # `compile` defines
188
194
  compile
189
195
 
190
- # If we're in Rails, add application url_helpers to the component context
191
- if defined?(Rails)
196
+ # If Rails application is loaded, add application url_helpers to the component context
197
+ # we need to check this to use this gem as a dependency
198
+ if defined?(Rails) && Rails.application
192
199
  child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
193
200
  end
194
201
 
@@ -207,16 +214,8 @@ module ViewComponent
207
214
  super
208
215
  end
209
216
 
210
- def call_method_name(variant)
211
- if variant.present? && variants.include?(variant)
212
- "call_#{variant}"
213
- else
214
- "call"
215
- end
216
- end
217
-
218
217
  def compiled?
219
- CompileCache.compiled?(self)
218
+ template_compiler.compiled?
220
219
  end
221
220
 
222
221
  # Compile templates to instance methods, assuming they haven't been compiled already.
@@ -224,75 +223,11 @@ module ViewComponent
224
223
  # Do as much work as possible in this step, as doing so reduces the amount
225
224
  # of work done each time a component is rendered.
226
225
  def compile(raise_errors: false)
227
- return if compiled?
228
-
229
- if template_errors.present?
230
- raise ViewComponent::TemplateError.new(template_errors) if raise_errors
231
- return false
232
- end
233
-
234
- if instance_methods(false).include?(:before_render_check)
235
- ActiveSupport::Deprecation.warn(
236
- "`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
237
- )
238
- end
239
-
240
- # Remove any existing singleton methods,
241
- # as Ruby warns when redefining a method.
242
- remove_possible_singleton_method(:variants)
243
- remove_possible_singleton_method(:collection_parameter)
244
- remove_possible_singleton_method(:collection_counter_parameter)
245
- remove_possible_singleton_method(:counter_argument_present?)
246
-
247
- define_singleton_method(:variants) do
248
- templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
249
- end
250
-
251
- define_singleton_method(:collection_parameter) do
252
- if provided_collection_parameter
253
- provided_collection_parameter
254
- else
255
- name.demodulize.underscore.chomp("_component").to_sym
256
- end
257
- end
258
-
259
- define_singleton_method(:collection_counter_parameter) do
260
- "#{collection_parameter}_counter".to_sym
261
- end
262
-
263
- define_singleton_method(:counter_argument_present?) do
264
- instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
265
- end
266
-
267
- validate_collection_parameter! if raise_errors
268
-
269
- # If template name annotations are turned on, a line is dynamically
270
- # added with a comment. In this case, we want to return a different
271
- # starting line number so errors that are raised will point to the
272
- # correct line in the component template.
273
- line_number =
274
- if ActionView::Base.respond_to?(:annotate_rendered_view_with_filenames) &&
275
- ActionView::Base.annotate_rendered_view_with_filenames
276
- -2
277
- else
278
- -1
279
- end
280
-
281
- templates.each do |template|
282
- # Remove existing compiled template methods,
283
- # as Ruby warns when redefining a method.
284
- method_name = call_method_name(template[:variant])
285
- undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)
286
-
287
- class_eval <<-RUBY, template[:path], line_number
288
- def #{method_name}
289
- @output_buffer = ActionView::OutputBuffer.new
290
- #{compiled_template(template[:path])}
291
- end
292
- RUBY
293
- end
226
+ template_compiler.compile(raise_errors: raise_errors)
227
+ end
294
228
 
295
- CompileCache.register self
229
+ def template_compiler
230
+ @_template_compiler ||= Compiler.new(self)
296
231
  end
297
232
 
298
233
  # we'll eventually want to update this to support other types
@@ -357,111 +292,6 @@ module ViewComponent
357
292
  @provided_collection_parameter ||= nil
358
293
  end
359
294
 
360
- def compiled_template(file_path)
361
- handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
362
- template = File.read(file_path)
363
-
364
- if handler.method(:call).parameters.length > 1
365
- handler.call(self, template)
366
- else
367
- handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
368
- end
369
- end
370
-
371
- def inline_calls
372
- @inline_calls ||=
373
- begin
374
- # Fetch only ViewComponent ancestor classes to limit the scope of
375
- # finding inline calls
376
- view_component_ancestors =
377
- ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - included_modules
378
-
379
- view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
380
- end
381
- end
382
-
383
- def inline_calls_defined_on_self
384
- @inline_calls_defined_on_self ||= instance_methods(false).grep(/^call/)
385
- end
386
-
387
- def matching_views_in_source_location
388
- return [] unless source_location
389
-
390
- location_without_extension = source_location.chomp(File.extname(source_location))
391
-
392
- extensions = ActionView::Template.template_handler_extensions.join(",")
393
-
394
- # view files in the same directory as the component
395
- sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
396
-
397
- # view files in a directory named like the component
398
- directory = File.dirname(source_location)
399
- filename = File.basename(source_location, ".rb")
400
- component_name = name.demodulize.underscore
401
-
402
- sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
403
-
404
- (sidecar_files - [source_location] + sidecar_directory_files)
405
- end
406
-
407
- def templates
408
- @templates ||=
409
- matching_views_in_source_location.each_with_object([]) do |path, memo|
410
- pieces = File.basename(path).split(".")
411
-
412
- memo << {
413
- path: path,
414
- variant: pieces.second.split("+").second&.to_sym,
415
- handler: pieces.last
416
- }
417
- end
418
- end
419
-
420
- def template_errors
421
- @template_errors ||=
422
- begin
423
- errors = []
424
-
425
- if (templates + inline_calls).empty?
426
- errors << "Could not find a template file or inline render method for #{self}."
427
- end
428
-
429
- if templates.count { |template| template[:variant].nil? } > 1
430
- errors << "More than one template found for #{self}. There can only be one default template file per component."
431
- end
432
-
433
- invalid_variants = templates
434
- .group_by { |template| template[:variant] }
435
- .map { |variant, grouped| variant if grouped.length > 1 }
436
- .compact
437
- .sort
438
-
439
- unless invalid_variants.empty?
440
- 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."
441
- end
442
-
443
- if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
444
- errors << "Template file and inline render method found for #{self}. There can only be a template file or inline render method per component."
445
- end
446
-
447
- duplicate_template_file_and_inline_variant_calls =
448
- templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
449
-
450
- unless duplicate_template_file_and_inline_variant_calls.empty?
451
- count = duplicate_template_file_and_inline_variant_calls.count
452
-
453
- 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."
454
- end
455
-
456
- errors
457
- end
458
- end
459
-
460
- def variants_from_inline_calls(calls)
461
- calls.reject { |call| call == :call }.map do |variant_call|
462
- variant_call.to_s.sub("call_", "").to_sym
463
- end
464
- end
465
295
  end
466
296
 
467
297
  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