view_component 3.23.2 → 4.0.2
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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/view_component/preview_actions.rb +11 -14
- data/app/controllers/view_components_system_test_controller.rb +15 -20
- data/app/views/test_mailer/test_asset_email.html.erb +1 -0
- data/app/views/test_mailer/test_url_email.html.erb +1 -0
- data/app/views/view_components/preview.html.erb +1 -9
- data/docs/CHANGELOG.md +422 -0
- data/lib/{rails/generators → generators/view_component}/abstract_generator.rb +2 -2
- data/lib/{rails/generators → generators/view_component}/component/component_generator.rb +16 -3
- data/lib/{rails/generators → generators/view_component}/component/templates/component.rb.tt +6 -1
- data/lib/{rails/generators/erb/component_generator.rb → generators/view_component/erb/erb_generator.rb} +4 -3
- data/lib/{rails/generators/haml/component_generator.rb → generators/view_component/haml/haml_generator.rb} +3 -3
- data/lib/{rails/generators/locale/component_generator.rb → generators/view_component/locale/locale_generator.rb} +3 -3
- data/lib/{rails/generators/preview/component_generator.rb → generators/view_component/preview/preview_generator.rb} +3 -3
- data/lib/{rails/generators/rspec/component_generator.rb → generators/view_component/rspec/rspec_generator.rb} +3 -3
- data/lib/{rails/generators/slim/component_generator.rb → generators/view_component/slim/slim_generator.rb} +3 -3
- data/lib/{rails/generators/stimulus/component_generator.rb → generators/view_component/stimulus/stimulus_generator.rb} +3 -3
- data/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb +11 -0
- data/lib/{rails/generators/test_unit/component_generator.rb → generators/view_component/test_unit/test_unit_generator.rb} +2 -2
- data/lib/view_component/base.rb +178 -158
- data/lib/view_component/collection.rb +19 -25
- data/lib/view_component/compiler.rb +52 -79
- data/lib/view_component/config.rb +51 -85
- data/lib/view_component/configurable.rb +1 -1
- data/lib/view_component/deprecation.rb +1 -1
- data/lib/view_component/engine.rb +37 -107
- data/lib/view_component/errors.rb +16 -34
- data/lib/view_component/inline_template.rb +3 -4
- data/lib/view_component/instrumentation.rb +4 -10
- data/lib/view_component/preview.rb +4 -11
- data/lib/view_component/request_details.rb +30 -0
- data/lib/view_component/slot.rb +6 -13
- data/lib/view_component/slotable.rb +82 -77
- data/lib/view_component/system_spec_helpers.rb +11 -0
- data/lib/view_component/system_test_helpers.rb +1 -2
- data/lib/view_component/template.rb +106 -83
- data/lib/view_component/test_helpers.rb +46 -43
- data/lib/view_component/translatable.rb +33 -32
- data/lib/view_component/version.rb +2 -2
- data/lib/view_component.rb +8 -6
- metadata +31 -559
- data/app/assets/vendor/prism.css +0 -4
- data/app/assets/vendor/prism.min.js +0 -12
- data/app/helpers/preview_helper.rb +0 -85
- data/app/views/view_components/_preview_source.html.erb +0 -17
- data/lib/rails/generators/tailwindcss/component_generator.rb +0 -11
- data/lib/view_component/capture_compatibility.rb +0 -44
- data/lib/view_component/component_error.rb +0 -6
- data/lib/view_component/rails/tasks/view_component.rake +0 -20
- data/lib/view_component/render_component_helper.rb +0 -10
- data/lib/view_component/render_component_to_string_helper.rb +0 -9
- data/lib/view_component/render_monkey_patch.rb +0 -13
- data/lib/view_component/render_to_string_monkey_patch.rb +0 -13
- data/lib/view_component/rendering_component_helper.rb +0 -9
- data/lib/view_component/rendering_monkey_patch.rb +0 -13
- data/lib/view_component/slotable_default.rb +0 -20
- data/lib/view_component/use_helpers.rb +0 -42
- /data/lib/{rails/generators → generators/view_component}/erb/templates/component.html.erb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/haml/templates/component.html.haml.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/preview/templates/component_preview.rb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/rspec/templates/component_spec.rb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/slim/templates/component.html.slim.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.js.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.ts.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/tailwindcss/templates/component.html.erb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/test_unit/templates/component_test.rb.tt +0 -0
data/lib/view_component/base.rb
CHANGED
@@ -9,15 +9,27 @@ require "view_component/config"
|
|
9
9
|
require "view_component/errors"
|
10
10
|
require "view_component/inline_template"
|
11
11
|
require "view_component/preview"
|
12
|
+
require "view_component/request_details"
|
12
13
|
require "view_component/slotable"
|
13
|
-
require "view_component/slotable_default"
|
14
14
|
require "view_component/template"
|
15
15
|
require "view_component/translatable"
|
16
16
|
require "view_component/with_content_helper"
|
17
|
-
|
17
|
+
|
18
|
+
module ActionView
|
19
|
+
class OutputBuffer
|
20
|
+
def with_buffer(buf = nil)
|
21
|
+
new_buffer = buf || +""
|
22
|
+
old_buffer, @raw_buffer = @raw_buffer, new_buffer
|
23
|
+
yield
|
24
|
+
new_buffer
|
25
|
+
ensure
|
26
|
+
@raw_buffer = old_buffer
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
18
30
|
|
19
31
|
module ViewComponent
|
20
|
-
class Base
|
32
|
+
class Base
|
21
33
|
class << self
|
22
34
|
delegate(*ViewComponent::Config.defaults.keys, to: :config)
|
23
35
|
|
@@ -34,26 +46,33 @@ module ViewComponent
|
|
34
46
|
end
|
35
47
|
end
|
36
48
|
|
49
|
+
include ActionView::Helpers
|
50
|
+
include Rails.application.routes.url_helpers if defined?(Rails) && Rails.application
|
51
|
+
include ERB::Escape
|
52
|
+
include ActiveSupport::CoreExt::ERBUtil
|
53
|
+
|
37
54
|
include ViewComponent::InlineTemplate
|
38
|
-
include ViewComponent::UseHelpers
|
39
55
|
include ViewComponent::Slotable
|
40
56
|
include ViewComponent::Translatable
|
41
57
|
include ViewComponent::WithContentHelper
|
42
58
|
|
43
|
-
RESERVED_PARAMETER = :content
|
44
|
-
VC_INTERNAL_DEFAULT_FORMAT = :html
|
45
|
-
|
46
59
|
# For CSRF authenticity tokens in forms
|
47
60
|
delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
|
48
61
|
|
62
|
+
# HTML construction methods
|
63
|
+
delegate :output_buffer, :lookup_context, :view_renderer, :view_flow, to: :helpers
|
64
|
+
|
65
|
+
# For Turbo::StreamsHelper
|
66
|
+
delegate :formats, :formats=, to: :helpers
|
67
|
+
|
49
68
|
# For Content Security Policy nonces
|
50
69
|
delegate :content_security_policy_nonce, to: :helpers
|
51
70
|
|
52
71
|
# Config option that strips trailing whitespace in templates before compiling them.
|
53
|
-
class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false
|
54
|
-
self.__vc_strip_trailing_whitespace = false # class_attribute:default doesn't work until Rails 5.2
|
72
|
+
class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false, default: false
|
55
73
|
|
56
74
|
attr_accessor :__vc_original_view_context
|
75
|
+
attr_reader :current_template
|
57
76
|
|
58
77
|
# Components render in their own view context. Helpers and other functionality
|
59
78
|
# require a reference to the original Rails view context, an instance of
|
@@ -65,7 +84,14 @@ module ViewComponent
|
|
65
84
|
# @param view_context [ActionView::Base] The original view context.
|
66
85
|
# @return [void]
|
67
86
|
def set_original_view_context(view_context)
|
68
|
-
|
87
|
+
# noop
|
88
|
+
end
|
89
|
+
|
90
|
+
using RequestDetails
|
91
|
+
|
92
|
+
# Including `Rails.application.routes.url_helpers` defines an initializer that accepts (...),
|
93
|
+
# so we have to define our own empty initializer to overwrite it.
|
94
|
+
def initialize
|
69
95
|
end
|
70
96
|
|
71
97
|
# Entrypoint for rendering components.
|
@@ -77,31 +103,28 @@ module ViewComponent
|
|
77
103
|
#
|
78
104
|
# @return [String]
|
79
105
|
def render_in(view_context, &block)
|
80
|
-
self.class.
|
106
|
+
self.class.__vc_compile(raise_errors: true)
|
81
107
|
|
82
108
|
@view_context = view_context
|
109
|
+
@old_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
83
110
|
self.__vc_original_view_context ||= view_context
|
84
111
|
|
85
|
-
@output_buffer =
|
112
|
+
@output_buffer = view_context.output_buffer
|
86
113
|
|
87
114
|
@lookup_context ||= view_context.lookup_context
|
88
115
|
|
89
|
-
# required for path helpers in older Rails versions
|
90
|
-
@view_renderer ||= view_context.view_renderer
|
91
|
-
|
92
116
|
# For content_for
|
93
117
|
@view_flow ||= view_context.view_flow
|
94
118
|
|
95
119
|
# For i18n
|
96
120
|
@virtual_path ||= virtual_path
|
97
121
|
|
98
|
-
#
|
99
|
-
@
|
122
|
+
# Describes the inferred request constraints (locales, formats, variants)
|
123
|
+
@__vc_requested_details ||= @lookup_context.vc_requested_details
|
100
124
|
|
101
125
|
# For caching, such as #cache_if
|
102
126
|
@current_template = nil unless defined?(@current_template)
|
103
127
|
old_current_template = @current_template
|
104
|
-
@current_template = self
|
105
128
|
|
106
129
|
if block && defined?(@__vc_content_set_by_with_content)
|
107
130
|
raise DuplicateContentError.new(self.class.name)
|
@@ -113,18 +136,35 @@ module ViewComponent
|
|
113
136
|
before_render
|
114
137
|
|
115
138
|
if render?
|
116
|
-
|
139
|
+
value = nil
|
140
|
+
|
141
|
+
@output_buffer.with_buffer do
|
142
|
+
@view_context.instance_variable_set(:@virtual_path, virtual_path)
|
143
|
+
|
144
|
+
rendered_template =
|
145
|
+
around_render do
|
146
|
+
render_template_for(@__vc_requested_details).to_s
|
147
|
+
end
|
148
|
+
|
149
|
+
# Avoid allocating new string when output_preamble and output_postamble are blank
|
150
|
+
value = if output_preamble.blank? && output_postamble.blank?
|
151
|
+
rendered_template
|
152
|
+
else
|
153
|
+
__vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble
|
154
|
+
end
|
155
|
+
end
|
117
156
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
else
|
122
|
-
safe_output_preamble + rendered_template + safe_output_postamble
|
157
|
+
if ActionView::Base.annotate_rendered_view_with_filenames && current_template.inline_call? && request&.format == :html
|
158
|
+
identifier = defined?(Rails.root) ? self.class.identifier.sub("#{Rails.root}/", "") : self.class.identifier
|
159
|
+
value = "<!-- BEGIN #{identifier} -->".html_safe + value + "<!-- END #{identifier} -->".html_safe
|
123
160
|
end
|
161
|
+
|
162
|
+
value
|
124
163
|
else
|
125
164
|
""
|
126
165
|
end
|
127
166
|
ensure
|
167
|
+
view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
|
128
168
|
@current_template = old_current_template
|
129
169
|
end
|
130
170
|
|
@@ -161,7 +201,7 @@ module ViewComponent
|
|
161
201
|
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
|
162
202
|
@__vc_parent_render_level += 1
|
163
203
|
|
164
|
-
target_render.bind_call(self, @
|
204
|
+
target_render.bind_call(self, @__vc_requested_details)
|
165
205
|
ensure
|
166
206
|
@__vc_parent_render_level -= 1
|
167
207
|
end
|
@@ -189,6 +229,14 @@ module ViewComponent
|
|
189
229
|
# noop
|
190
230
|
end
|
191
231
|
|
232
|
+
# Called around rendering the component. Override to wrap the rendering of a
|
233
|
+
# component in custom instrumentation, etc.
|
234
|
+
#
|
235
|
+
# @return [void]
|
236
|
+
def around_render
|
237
|
+
yield
|
238
|
+
end
|
239
|
+
|
192
240
|
# Override to determine whether the ViewComponent should render.
|
193
241
|
#
|
194
242
|
# @return [Boolean]
|
@@ -196,25 +244,34 @@ module ViewComponent
|
|
196
244
|
true
|
197
245
|
end
|
198
246
|
|
199
|
-
# Override the ActionView::Base initializer so that components
|
200
|
-
# do not need to define their own initializers.
|
201
|
-
# @private
|
202
|
-
def initialize(*)
|
203
|
-
end
|
204
|
-
|
205
247
|
# Re-use original view_context if we're not rendering a component.
|
206
248
|
#
|
207
|
-
#
|
208
|
-
#
|
209
|
-
#
|
249
|
+
# As of v4, ViewComponent::Base re-uses the existing view context created
|
250
|
+
# by ActionView, meaning the current view context and the original view
|
251
|
+
# context are the same object. set_original_view_context is still called
|
252
|
+
# to maintain backwards compatibility.
|
210
253
|
#
|
211
254
|
# @private
|
212
255
|
def render(options = {}, args = {}, &block)
|
213
256
|
if options.respond_to?(:set_original_view_context)
|
214
257
|
options.set_original_view_context(self.__vc_original_view_context)
|
215
|
-
|
258
|
+
|
259
|
+
# We assume options is a component, so there's no need to evaluate the
|
260
|
+
# block in the view context as we do below.
|
261
|
+
@view_context.render(options, args, &block)
|
262
|
+
elsif block
|
263
|
+
__vc_original_view_context.render(options, args) do
|
264
|
+
# Partials are rendered to their own buffer and do not append to the
|
265
|
+
# original @output_buffer we retain a reference to in #render_in. This
|
266
|
+
# is a problem since the block passed to us here in the #render method
|
267
|
+
# is evaluated within the context of ViewComponent::Base, and thus
|
268
|
+
# appends to the original @output_buffer. To avoid this, we evaluate the
|
269
|
+
# block in the view context instead, which will append to the output buffer
|
270
|
+
# created for the partial.
|
271
|
+
__vc_original_view_context.instance_exec(&block)
|
272
|
+
end
|
216
273
|
else
|
217
|
-
__vc_original_view_context.render(options, args
|
274
|
+
__vc_original_view_context.render(options, args)
|
218
275
|
end
|
219
276
|
end
|
220
277
|
|
@@ -274,11 +331,12 @@ module ViewComponent
|
|
274
331
|
[]
|
275
332
|
end
|
276
333
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
334
|
+
if Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
335
|
+
# Rails expects us to define `format` on all renderables,
|
336
|
+
# but we do not know the `format` of a ViewComponent until runtime.
|
337
|
+
def format
|
338
|
+
nil
|
339
|
+
end
|
282
340
|
end
|
283
341
|
|
284
342
|
# The current request. Use sparingly as doing so introduces coupling that
|
@@ -289,10 +347,9 @@ module ViewComponent
|
|
289
347
|
__vc_request
|
290
348
|
end
|
291
349
|
|
292
|
-
# Enables consumers to override request/@request
|
293
|
-
#
|
294
350
|
# @private
|
295
351
|
def __vc_request
|
352
|
+
# The current request (if present, as mailers/jobs/etc do not have a request)
|
296
353
|
@__vc_request ||= controller.request if controller.respond_to?(:request)
|
297
354
|
end
|
298
355
|
|
@@ -305,7 +362,9 @@ module ViewComponent
|
|
305
362
|
|
306
363
|
@__vc_content =
|
307
364
|
if __vc_render_in_block_provided?
|
308
|
-
|
365
|
+
with_original_virtual_path do
|
366
|
+
view_context.capture(self, &@__vc_render_in_block)
|
367
|
+
end
|
309
368
|
elsif __vc_content_set_by_with_content_defined?
|
310
369
|
@__vc_content_set_by_with_content
|
311
370
|
end
|
@@ -318,6 +377,14 @@ module ViewComponent
|
|
318
377
|
__vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
|
319
378
|
end
|
320
379
|
|
380
|
+
# @private
|
381
|
+
def with_original_virtual_path
|
382
|
+
@view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
|
383
|
+
yield
|
384
|
+
ensure
|
385
|
+
@view_context.instance_variable_set(:@virtual_path, virtual_path)
|
386
|
+
end
|
387
|
+
|
321
388
|
private
|
322
389
|
|
323
390
|
attr_reader :view_context
|
@@ -330,12 +397,8 @@ module ViewComponent
|
|
330
397
|
defined?(@__vc_content_set_by_with_content)
|
331
398
|
end
|
332
399
|
|
333
|
-
def
|
334
|
-
|
335
|
-
end
|
336
|
-
|
337
|
-
def maybe_escape_html(text)
|
338
|
-
return text if __vc_request && !__vc_request.format.html?
|
400
|
+
def __vc_maybe_escape_html(text)
|
401
|
+
return text if @current_template && !@current_template.html?
|
339
402
|
return text if text.blank?
|
340
403
|
|
341
404
|
if text.html_safe?
|
@@ -346,54 +409,18 @@ module ViewComponent
|
|
346
409
|
end
|
347
410
|
end
|
348
411
|
|
349
|
-
def
|
350
|
-
|
412
|
+
def __vc_safe_output_preamble
|
413
|
+
__vc_maybe_escape_html(output_preamble) do
|
351
414
|
Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe preamble. The preamble will be automatically escaped, but you may want to investigate.")
|
352
415
|
end
|
353
416
|
end
|
354
417
|
|
355
|
-
def
|
356
|
-
|
418
|
+
def __vc_safe_output_postamble
|
419
|
+
__vc_maybe_escape_html(output_postamble) do
|
357
420
|
Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe postamble. The postamble will be automatically escaped, but you may want to investigate.")
|
358
421
|
end
|
359
422
|
end
|
360
423
|
|
361
|
-
# Set the controller used for testing components:
|
362
|
-
#
|
363
|
-
# ```ruby
|
364
|
-
# config.view_component.test_controller = "MyTestController"
|
365
|
-
# ```
|
366
|
-
#
|
367
|
-
# Defaults to `nil`. If this is falsy, `"ApplicationController"` is used. Can also be
|
368
|
-
# configured on a per-test basis using `with_controller_class`.
|
369
|
-
#
|
370
|
-
|
371
|
-
# Set if render monkey patches should be included or not in Rails <6.1:
|
372
|
-
#
|
373
|
-
# ```ruby
|
374
|
-
# config.view_component.render_monkey_patch_enabled = false
|
375
|
-
# ```
|
376
|
-
#
|
377
|
-
|
378
|
-
# Path for component files
|
379
|
-
#
|
380
|
-
# ```ruby
|
381
|
-
# config.view_component.view_component_path = "app/my_components"
|
382
|
-
# ```
|
383
|
-
#
|
384
|
-
# Defaults to `nil`. If this is falsy, `app/components` is used.
|
385
|
-
#
|
386
|
-
|
387
|
-
# Parent class for generated components
|
388
|
-
#
|
389
|
-
# ```ruby
|
390
|
-
# config.view_component.component_parent_class = "MyBaseComponent"
|
391
|
-
# ```
|
392
|
-
#
|
393
|
-
# Defaults to nil. If this is falsy, generators will use
|
394
|
-
# "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
|
395
|
-
#
|
396
|
-
|
397
424
|
# Configuration for generators.
|
398
425
|
#
|
399
426
|
# All options under this namespace default to `false` unless otherwise
|
@@ -451,6 +478,18 @@ module ViewComponent
|
|
451
478
|
# ```
|
452
479
|
#
|
453
480
|
# Defaults to `false`.
|
481
|
+
#
|
482
|
+
# #### ßparent_class
|
483
|
+
#
|
484
|
+
# Parent class for generated components
|
485
|
+
#
|
486
|
+
# ```ruby
|
487
|
+
# config.view_component.generate.parent_class = "MyBaseComponent"
|
488
|
+
# ```
|
489
|
+
#
|
490
|
+
# Defaults to nil. If this is falsy, generators will use
|
491
|
+
# "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
|
492
|
+
#
|
454
493
|
|
455
494
|
class << self
|
456
495
|
# The file path of the component Ruby file.
|
@@ -519,50 +558,43 @@ module ViewComponent
|
|
519
558
|
Collection.new(self, collection, spacer_component, **args)
|
520
559
|
end
|
521
560
|
|
561
|
+
# @private
|
562
|
+
def __vc_compile(raise_errors: false, force: false)
|
563
|
+
__vc_compiler.compile(raise_errors: raise_errors, force: force)
|
564
|
+
end
|
565
|
+
|
522
566
|
# @private
|
523
567
|
def inherited(child)
|
524
568
|
# Compile so child will inherit compiled `call_*` template methods that
|
525
569
|
# `compile` defines
|
526
|
-
|
570
|
+
__vc_compile
|
527
571
|
|
528
572
|
# Give the child its own personal #render_template_for to protect against the case when
|
529
573
|
# eager loading is disabled and the parent component is rendered before the child. In
|
530
574
|
# such a scenario, the parent will override ViewComponent::Base#render_template_for,
|
531
575
|
# meaning it will not be called for any children and thus not compile their templates.
|
532
|
-
if !child.instance_methods(false).include?(:render_template_for) && !child.
|
576
|
+
if !child.instance_methods(false).include?(:render_template_for) && !child.__vc_compiled?
|
533
577
|
child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
534
|
-
def render_template_for(
|
578
|
+
def render_template_for(requested_details)
|
535
579
|
# Force compilation here so the compiler always redefines render_template_for.
|
536
580
|
# This is mostly a safeguard to prevent infinite recursion.
|
537
|
-
self.class.
|
538
|
-
# .
|
539
|
-
render_template_for(
|
581
|
+
self.class.__vc_compile(raise_errors: true, force: true)
|
582
|
+
# .__vc_compile replaces this method; call the new one
|
583
|
+
render_template_for(requested_details)
|
540
584
|
end
|
541
585
|
RUBY
|
542
586
|
end
|
543
587
|
|
544
|
-
# If Rails application is loaded, add application url_helpers to the component context
|
545
|
-
# we need to check this to use this gem as a dependency
|
546
|
-
if defined?(Rails) && Rails.application && !(child < Rails.application.routes.url_helpers)
|
547
|
-
child.include Rails.application.routes.url_helpers
|
548
|
-
end
|
549
|
-
|
550
588
|
# Derive the source location of the component Ruby file from the call stack.
|
551
589
|
# We need to ignore `inherited` frames here as they indicate that `inherited`
|
552
590
|
# has been re-defined by the consuming application, likely in ApplicationComponent.
|
553
591
|
# We use `base_label` method here instead of `label` to avoid cases where the method
|
554
592
|
# owner is included in a prefix like `ApplicationComponent.inherited`.
|
555
593
|
child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
|
556
|
-
|
557
|
-
# If Rails application is loaded, removes the first part of the path and the extension.
|
558
|
-
if defined?(Rails) && Rails.application
|
559
|
-
child.virtual_path = child.identifier.gsub(
|
560
|
-
/(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, ""
|
561
|
-
)
|
562
|
-
end
|
594
|
+
child.virtual_path = child.name&.underscore
|
563
595
|
|
564
596
|
# Set collection parameter to the extended component
|
565
|
-
child.with_collection_parameter
|
597
|
+
child.with_collection_parameter(__vc_provided_collection_parameter)
|
566
598
|
|
567
599
|
if instance_methods(false).include?(:render_template_for)
|
568
600
|
vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
|
@@ -575,22 +607,17 @@ module ViewComponent
|
|
575
607
|
end
|
576
608
|
|
577
609
|
# @private
|
578
|
-
def
|
579
|
-
|
580
|
-
end
|
581
|
-
|
582
|
-
# @private
|
583
|
-
def ensure_compiled
|
584
|
-
compile unless compiled?
|
610
|
+
def __vc_compiled?
|
611
|
+
__vc_compiler.compiled?
|
585
612
|
end
|
586
613
|
|
587
614
|
# @private
|
588
|
-
def
|
589
|
-
|
615
|
+
def __vc_ensure_compiled
|
616
|
+
__vc_compile unless __vc_compiled?
|
590
617
|
end
|
591
618
|
|
592
619
|
# @private
|
593
|
-
def
|
620
|
+
def __vc_compiler
|
594
621
|
@__vc_compiler ||= Compiler.new(self)
|
595
622
|
end
|
596
623
|
|
@@ -602,8 +629,8 @@ module ViewComponent
|
|
602
629
|
#
|
603
630
|
# @param parameter [Symbol] The parameter name used when rendering elements of a collection.
|
604
631
|
def with_collection_parameter(parameter)
|
605
|
-
@
|
606
|
-
@
|
632
|
+
@__vc_provided_collection_parameter = parameter
|
633
|
+
@__vc_initialize_parameters = nil
|
607
634
|
end
|
608
635
|
|
609
636
|
# Strips trailing whitespace from templates before compiling them.
|
@@ -632,18 +659,11 @@ module ViewComponent
|
|
632
659
|
# is accepted, as support for collection
|
633
660
|
# rendering is optional.
|
634
661
|
# @private
|
635
|
-
def
|
636
|
-
parameter = validate_default ?
|
662
|
+
def __vc_validate_collection_parameter!(validate_default: false)
|
663
|
+
parameter = validate_default ? __vc_collection_parameter : __vc_provided_collection_parameter
|
637
664
|
|
638
665
|
return unless parameter
|
639
|
-
return if
|
640
|
-
|
641
|
-
# If Ruby can't parse the component class, then the initialize
|
642
|
-
# parameters will be empty and ViewComponent will not be able to render
|
643
|
-
# the component.
|
644
|
-
if initialize_parameters.empty?
|
645
|
-
raise EmptyOrInvalidInitializerError.new(name, parameter)
|
646
|
-
end
|
666
|
+
return if __vc_initialize_parameter_names.include?(parameter) || __vc_splatted_keyword_argument_present?
|
647
667
|
|
648
668
|
raise MissingCollectionArgumentError.new(name, parameter)
|
649
669
|
end
|
@@ -652,58 +672,58 @@ module ViewComponent
|
|
652
672
|
# invalid parameters that could override the framework's
|
653
673
|
# methods.
|
654
674
|
# @private
|
655
|
-
def
|
656
|
-
return unless
|
675
|
+
def __vc_validate_initialization_parameters!
|
676
|
+
return unless __vc_initialize_parameter_names.include?(:content)
|
657
677
|
|
658
|
-
raise ReservedParameterError.new(name,
|
678
|
+
raise ReservedParameterError.new(name, :content)
|
659
679
|
end
|
660
680
|
|
661
681
|
# @private
|
662
|
-
def
|
663
|
-
|
682
|
+
def __vc_collection_parameter
|
683
|
+
@__vc_provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym
|
664
684
|
end
|
665
685
|
|
666
686
|
# @private
|
667
|
-
def
|
668
|
-
:"#{
|
687
|
+
def __vc_collection_counter_parameter
|
688
|
+
@__vc_collection_counter_parameter ||= :"#{__vc_collection_parameter}_counter"
|
669
689
|
end
|
670
690
|
|
671
691
|
# @private
|
672
|
-
def
|
673
|
-
|
692
|
+
def __vc_counter_argument_present?
|
693
|
+
__vc_initialize_parameter_names.include?(__vc_collection_counter_parameter)
|
674
694
|
end
|
675
695
|
|
676
696
|
# @private
|
677
|
-
def
|
678
|
-
:"#{
|
697
|
+
def __vc_collection_iteration_parameter
|
698
|
+
@__vc_collection_iteration_parameter ||= :"#{__vc_collection_parameter}_iteration"
|
679
699
|
end
|
680
700
|
|
681
701
|
# @private
|
682
|
-
def
|
683
|
-
|
702
|
+
def __vc_iteration_argument_present?
|
703
|
+
__vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter)
|
684
704
|
end
|
685
705
|
|
686
706
|
private
|
687
707
|
|
688
|
-
def
|
689
|
-
|
690
|
-
!initialize_parameters.include?([:keyrest, :**]) # Un-named splatted keyword args don't count!
|
708
|
+
def __vc_splatted_keyword_argument_present?
|
709
|
+
__vc_initialize_parameters.flatten.include?(:keyrest)
|
691
710
|
end
|
692
711
|
|
693
|
-
def
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
712
|
+
def __vc_initialize_parameter_names
|
713
|
+
@__vc_initialize_parameter_names ||=
|
714
|
+
if respond_to?(:attribute_names)
|
715
|
+
attribute_names.map(&:to_sym)
|
716
|
+
else
|
717
|
+
__vc_initialize_parameters.map(&:last)
|
718
|
+
end
|
699
719
|
end
|
700
720
|
|
701
|
-
def
|
702
|
-
@
|
721
|
+
def __vc_initialize_parameters
|
722
|
+
@__vc_initialize_parameters ||= instance_method(:initialize).parameters
|
703
723
|
end
|
704
724
|
|
705
|
-
def
|
706
|
-
@
|
725
|
+
def __vc_provided_collection_parameter
|
726
|
+
@__vc_provided_collection_parameter ||= nil
|
707
727
|
end
|
708
728
|
end
|
709
729
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "action_view/renderer/collection_renderer"
|
3
|
+
require "action_view/renderer/collection_renderer"
|
4
4
|
|
5
5
|
module ViewComponent
|
6
6
|
class Collection
|
@@ -9,25 +9,32 @@ module ViewComponent
|
|
9
9
|
|
10
10
|
delegate :size, to: :@collection
|
11
11
|
|
12
|
-
attr_accessor :__vc_original_view_context
|
13
|
-
|
14
|
-
def set_original_view_context(view_context)
|
15
|
-
self.__vc_original_view_context = view_context
|
16
|
-
end
|
17
|
-
|
18
12
|
def render_in(view_context, &block)
|
19
13
|
components.map do |component|
|
20
|
-
component.set_original_view_context(__vc_original_view_context)
|
21
14
|
component.render_in(view_context, &block)
|
22
15
|
end.join(rendered_spacer(view_context)).html_safe
|
23
16
|
end
|
24
17
|
|
18
|
+
def each(&block)
|
19
|
+
components.each(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
if Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
23
|
+
# Rails expects us to define `format` on all renderables,
|
24
|
+
# but we do not know the `format` of a ViewComponent until runtime.
|
25
|
+
def format
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
25
32
|
def components
|
26
33
|
return @components if defined? @components
|
27
34
|
|
28
35
|
iterator = ActionView::PartialIteration.new(@collection.size)
|
29
36
|
|
30
|
-
component.
|
37
|
+
component.__vc_validate_collection_parameter!(validate_default: true)
|
31
38
|
|
32
39
|
@components = @collection.map do |item|
|
33
40
|
component.new(**component_options(item, iterator)).tap do |component|
|
@@ -36,18 +43,6 @@ module ViewComponent
|
|
36
43
|
end
|
37
44
|
end
|
38
45
|
|
39
|
-
def each(&block)
|
40
|
-
components.each(&block)
|
41
|
-
end
|
42
|
-
|
43
|
-
# Rails expects us to define `format` on all renderables,
|
44
|
-
# but we do not know the `format` of a ViewComponent until runtime.
|
45
|
-
def format
|
46
|
-
nil
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
46
|
def initialize(component, object, spacer_component, **options)
|
52
47
|
@component = component
|
53
48
|
@collection = collection_variable(object || [])
|
@@ -64,16 +59,15 @@ module ViewComponent
|
|
64
59
|
end
|
65
60
|
|
66
61
|
def component_options(item, iterator)
|
67
|
-
item_options = {component.
|
68
|
-
item_options[component.
|
69
|
-
item_options[component.
|
62
|
+
item_options = {component.__vc_collection_parameter => item}
|
63
|
+
item_options[component.__vc_collection_counter_parameter] = iterator.index if component.__vc_counter_argument_present?
|
64
|
+
item_options[component.__vc_collection_iteration_parameter] = iterator.dup if component.__vc_iteration_argument_present?
|
70
65
|
|
71
66
|
@options.merge(item_options)
|
72
67
|
end
|
73
68
|
|
74
69
|
def rendered_spacer(view_context)
|
75
70
|
if @spacer_component
|
76
|
-
@spacer_component.set_original_view_context(__vc_original_view_context)
|
77
71
|
@spacer_component.render_in(view_context)
|
78
72
|
else
|
79
73
|
""
|