view_component 2.83.0 → 3.21.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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/view_component/preview_actions.rb +5 -1
- data/app/controllers/view_components_system_test_controller.rb +24 -1
- data/app/helpers/preview_helper.rb +22 -4
- data/app/views/view_components/_preview_source.html.erb +2 -2
- data/docs/CHANGELOG.md +807 -1
- data/lib/rails/generators/abstract_generator.rb +9 -1
- data/lib/rails/generators/component/component_generator.rb +2 -1
- data/lib/rails/generators/component/templates/component.rb.tt +3 -2
- data/lib/rails/generators/erb/component_generator.rb +1 -1
- data/lib/rails/generators/locale/component_generator.rb +3 -3
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +2 -0
- data/lib/rails/generators/rspec/component_generator.rb +15 -3
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
- data/lib/rails/generators/stimulus/component_generator.rb +8 -3
- data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component/base.rb +169 -164
- data/lib/view_component/capture_compatibility.rb +44 -0
- data/lib/view_component/collection.rb +20 -8
- data/lib/view_component/compiler.rb +166 -207
- data/lib/view_component/config.rb +63 -14
- data/lib/view_component/deprecation.rb +1 -1
- data/lib/view_component/docs_builder_component.html.erb +5 -1
- data/lib/view_component/docs_builder_component.rb +28 -9
- data/lib/view_component/engine.rb +58 -28
- data/lib/view_component/errors.rb +240 -0
- data/lib/view_component/inline_template.rb +55 -0
- data/lib/view_component/instrumentation.rb +10 -2
- data/lib/view_component/preview.rb +7 -8
- data/lib/view_component/rails/tasks/view_component.rake +11 -2
- data/lib/view_component/slot.rb +119 -1
- data/lib/view_component/slotable.rb +394 -94
- data/lib/view_component/slotable_default.rb +20 -0
- data/lib/view_component/system_test_helpers.rb +5 -5
- data/lib/view_component/template.rb +134 -0
- data/lib/view_component/test_helpers.rb +138 -59
- data/lib/view_component/translatable.rb +45 -26
- data/lib/view_component/use_helpers.rb +42 -0
- data/lib/view_component/version.rb +4 -3
- data/lib/view_component/with_content_helper.rb +3 -8
- data/lib/view_component.rb +3 -12
- metadata +277 -38
- data/lib/view_component/content_areas.rb +0 -56
- data/lib/view_component/polymorphic_slots.rb +0 -103
- data/lib/view_component/preview_template_error.rb +0 -6
- data/lib/view_component/slot_v2.rb +0 -98
- data/lib/view_component/slotable_v2.rb +0 -391
- data/lib/view_component/template_error.rb +0 -9
data/lib/view_component/base.rb
CHANGED
@@ -6,13 +6,15 @@ require "view_component/collection"
|
|
6
6
|
require "view_component/compile_cache"
|
7
7
|
require "view_component/compiler"
|
8
8
|
require "view_component/config"
|
9
|
-
require "view_component/
|
10
|
-
require "view_component/
|
9
|
+
require "view_component/errors"
|
10
|
+
require "view_component/inline_template"
|
11
11
|
require "view_component/preview"
|
12
12
|
require "view_component/slotable"
|
13
|
-
require "view_component/
|
13
|
+
require "view_component/slotable_default"
|
14
|
+
require "view_component/template"
|
14
15
|
require "view_component/translatable"
|
15
16
|
require "view_component/with_content_helper"
|
17
|
+
require "view_component/use_helpers"
|
16
18
|
|
17
19
|
module ViewComponent
|
18
20
|
class Base < ActionView::Base
|
@@ -21,31 +23,26 @@ module ViewComponent
|
|
21
23
|
|
22
24
|
# Returns the current config.
|
23
25
|
#
|
24
|
-
# @return [
|
26
|
+
# @return [ActiveSupport::OrderedOptions]
|
25
27
|
def config
|
26
|
-
|
28
|
+
ViewComponent::Config.current
|
27
29
|
end
|
28
|
-
|
29
|
-
# Replaces the entire config. You shouldn't need to use this directly
|
30
|
-
# unless you're building a `ViewComponent::Config` elsewhere.
|
31
|
-
attr_writer :config
|
32
30
|
end
|
33
31
|
|
34
|
-
include ViewComponent::
|
35
|
-
include ViewComponent::
|
36
|
-
include ViewComponent::
|
32
|
+
include ViewComponent::InlineTemplate
|
33
|
+
include ViewComponent::UseHelpers
|
34
|
+
include ViewComponent::Slotable
|
37
35
|
include ViewComponent::Translatable
|
38
36
|
include ViewComponent::WithContentHelper
|
39
37
|
|
40
|
-
ViewContextCalledBeforeRenderError = Class.new(StandardError)
|
41
|
-
|
42
38
|
RESERVED_PARAMETER = :content
|
39
|
+
VC_INTERNAL_DEFAULT_FORMAT = :html
|
43
40
|
|
44
41
|
# For CSRF authenticity tokens in forms
|
45
42
|
delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
|
46
43
|
|
47
|
-
|
48
|
-
|
44
|
+
# For Content Security Policy nonces
|
45
|
+
delegate :content_security_policy_nonce, to: :helpers
|
49
46
|
|
50
47
|
# Config option that strips trailing whitespace in templates before compiling them.
|
51
48
|
class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false
|
@@ -66,23 +63,6 @@ module ViewComponent
|
|
66
63
|
self.__vc_original_view_context = view_context
|
67
64
|
end
|
68
65
|
|
69
|
-
# @!macro [attach] deprecated_generate_mattr_accessor
|
70
|
-
# @method generate_$1
|
71
|
-
# @deprecated Use `#generate.$1` instead. Will be removed in v3.0.0.
|
72
|
-
def self._deprecated_generate_mattr_accessor(name)
|
73
|
-
define_singleton_method("generate_#{name}".to_sym) do
|
74
|
-
generate.public_send(name)
|
75
|
-
end
|
76
|
-
define_singleton_method("generate_#{name}=".to_sym) do |value|
|
77
|
-
generate.public_send("#{name}=".to_sym, value)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
_deprecated_generate_mattr_accessor :distinct_locale_files
|
82
|
-
_deprecated_generate_mattr_accessor :locale
|
83
|
-
_deprecated_generate_mattr_accessor :sidecar
|
84
|
-
_deprecated_generate_mattr_accessor :stimulus_controller
|
85
|
-
|
86
66
|
# Entrypoint for rendering components.
|
87
67
|
#
|
88
68
|
# - `view_context`: ActionView context from calling view
|
@@ -119,9 +99,7 @@ module ViewComponent
|
|
119
99
|
@current_template = self
|
120
100
|
|
121
101
|
if block && defined?(@__vc_content_set_by_with_content)
|
122
|
-
raise
|
123
|
-
"which means that ViewComponent doesn't know which content to use.\n\n" \
|
124
|
-
"To fix this issue, use either `with_content` or a block."
|
102
|
+
raise DuplicateContentError.new(self.class.name)
|
125
103
|
end
|
126
104
|
|
127
105
|
@__vc_content_evaluated = false
|
@@ -130,11 +108,13 @@ module ViewComponent
|
|
130
108
|
before_render
|
131
109
|
|
132
110
|
if render?
|
133
|
-
|
134
|
-
|
135
|
-
|
111
|
+
rendered_template = render_template_for(@__vc_variant, __vc_request&.format&.to_sym).to_s
|
112
|
+
|
113
|
+
# Avoid allocating new string when output_preamble and output_postamble are blank
|
114
|
+
if output_preamble.blank? && output_postamble.blank?
|
115
|
+
rendered_template
|
136
116
|
else
|
137
|
-
|
117
|
+
safe_output_preamble + rendered_template + safe_output_postamble
|
138
118
|
end
|
139
119
|
else
|
140
120
|
""
|
@@ -144,20 +124,51 @@ module ViewComponent
|
|
144
124
|
end
|
145
125
|
|
146
126
|
# Subclass components that call `super` inside their template code will cause a
|
147
|
-
# double render if they emit the result
|
127
|
+
# double render if they emit the result.
|
148
128
|
#
|
149
129
|
# ```erb
|
150
130
|
# <%= super %> # double-renders
|
151
|
-
# <% super %> #
|
131
|
+
# <% super %> # doesn't double-render
|
152
132
|
# ```
|
153
133
|
#
|
154
|
-
#
|
134
|
+
# `super` also doesn't consider the current variant. `render_parent` renders the
|
135
|
+
# parent template considering the current variant and emits the result without
|
136
|
+
# double-rendering.
|
155
137
|
def render_parent
|
156
|
-
|
157
|
-
method(mtd).super_method.call
|
138
|
+
render_parent_to_string
|
158
139
|
nil
|
159
140
|
end
|
160
141
|
|
142
|
+
# Renders the parent component to a string and returns it. This method is meant
|
143
|
+
# to be used inside custom #call methods when a string result is desired, eg.
|
144
|
+
#
|
145
|
+
# ```ruby
|
146
|
+
# def call
|
147
|
+
# "<div>#{render_parent_to_string}</div>"
|
148
|
+
# end
|
149
|
+
# ```
|
150
|
+
#
|
151
|
+
# When rendering the parent inside an .erb template, use `#render_parent` instead.
|
152
|
+
def render_parent_to_string
|
153
|
+
@__vc_parent_render_level ||= 0 # ensure a good starting value
|
154
|
+
|
155
|
+
begin
|
156
|
+
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
|
157
|
+
@__vc_parent_render_level += 1
|
158
|
+
|
159
|
+
target_render.bind_call(self, @__vc_variant)
|
160
|
+
ensure
|
161
|
+
@__vc_parent_render_level -= 1
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Optional content to be returned before the rendered template.
|
166
|
+
#
|
167
|
+
# @return [String]
|
168
|
+
def output_preamble
|
169
|
+
@@default_output_preamble ||= "".html_safe
|
170
|
+
end
|
171
|
+
|
161
172
|
# Optional content to be returned after the rendered template.
|
162
173
|
#
|
163
174
|
# @return [String]
|
@@ -170,14 +181,6 @@ module ViewComponent
|
|
170
181
|
#
|
171
182
|
# @return [void]
|
172
183
|
def before_render
|
173
|
-
before_render_check
|
174
|
-
end
|
175
|
-
|
176
|
-
# Called after rendering the component.
|
177
|
-
#
|
178
|
-
# @deprecated Use `#before_render` instead. Will be removed in v3.0.0.
|
179
|
-
# @return [void]
|
180
|
-
def before_render_check
|
181
184
|
# noop
|
182
185
|
end
|
183
186
|
|
@@ -213,16 +216,7 @@ module ViewComponent
|
|
213
216
|
#
|
214
217
|
# @return [ActionController::Base]
|
215
218
|
def controller
|
216
|
-
if view_context.nil?
|
217
|
-
raise(
|
218
|
-
ViewContextCalledBeforeRenderError,
|
219
|
-
"`#controller` can't be used during initialization, as it depends " \
|
220
|
-
"on the view context that only exists once a ViewComponent is passed to " \
|
221
|
-
"the Rails render pipeline.\n\n" \
|
222
|
-
"It's sometimes possible to fix this issue by moving code dependent on " \
|
223
|
-
"`#controller` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
|
224
|
-
)
|
225
|
-
end
|
219
|
+
raise ControllerCalledBeforeRenderError if view_context.nil?
|
226
220
|
|
227
221
|
@__vc_controller ||= view_context.controller
|
228
222
|
end
|
@@ -232,16 +226,7 @@ module ViewComponent
|
|
232
226
|
#
|
233
227
|
# @return [ActionView::Base]
|
234
228
|
def helpers
|
235
|
-
if view_context.nil?
|
236
|
-
raise(
|
237
|
-
ViewContextCalledBeforeRenderError,
|
238
|
-
"`#helpers` can't be used during initialization, as it depends " \
|
239
|
-
"on the view context that only exists once a ViewComponent is passed to " \
|
240
|
-
"the Rails render pipeline.\n\n" \
|
241
|
-
"It's sometimes possible to fix this issue by moving code dependent on " \
|
242
|
-
"`#helpers` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
|
243
|
-
)
|
244
|
-
end
|
229
|
+
raise HelpersCalledBeforeRenderError if view_context.nil?
|
245
230
|
|
246
231
|
# Attempt to re-use the original view_context passed to the first
|
247
232
|
# component rendered in the rendering pipeline. This prevents the
|
@@ -253,6 +238,22 @@ module ViewComponent
|
|
253
238
|
@__vc_helpers ||= __vc_original_view_context || controller.view_context
|
254
239
|
end
|
255
240
|
|
241
|
+
if ::Rails.env.development? || ::Rails.env.test?
|
242
|
+
# @private
|
243
|
+
def method_missing(method_name, *args) # rubocop:disable Style/MissingRespondToMissing
|
244
|
+
super
|
245
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
246
|
+
e.set_backtrace e.backtrace.tap(&:shift)
|
247
|
+
raise e, <<~MESSAGE.chomp if view_context && e.is_a?(NameError) && helpers.respond_to?(method_name)
|
248
|
+
#{e.message}
|
249
|
+
|
250
|
+
You may be trying to call a method provided as a view helper. Did you mean `helpers.#{method_name}'?
|
251
|
+
MESSAGE
|
252
|
+
|
253
|
+
raise
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
256
257
|
# Exposes .virtual_path as an instance method
|
257
258
|
#
|
258
259
|
# @private
|
@@ -270,52 +271,64 @@ module ViewComponent
|
|
270
271
|
#
|
271
272
|
# @private
|
272
273
|
def format
|
273
|
-
# Ruby 2.6 throws a warning without checking `defined?`, 2.7 doesn't
|
274
274
|
@__vc_variant if defined?(@__vc_variant)
|
275
275
|
end
|
276
276
|
|
277
|
-
# Use the provided variant instead of the one determined by the current request.
|
278
|
-
#
|
279
|
-
# @deprecated Will be removed in v3.0.0.
|
280
|
-
# @param variant [Symbol] The variant to be used by the component.
|
281
|
-
# @return [self]
|
282
|
-
def with_variant(variant)
|
283
|
-
@__vc_variant = variant
|
284
|
-
|
285
|
-
self
|
286
|
-
end
|
287
|
-
deprecate :with_variant, deprecator: ViewComponent::Deprecation
|
288
|
-
|
289
277
|
# The current request. Use sparingly as doing so introduces coupling that
|
290
278
|
# inhibits encapsulation & reuse, often making testing difficult.
|
291
279
|
#
|
292
280
|
# @return [ActionDispatch::Request]
|
293
281
|
def request
|
294
|
-
|
282
|
+
__vc_request
|
295
283
|
end
|
296
284
|
|
297
|
-
|
298
|
-
|
299
|
-
|
285
|
+
# Enables consumers to override request/@request
|
286
|
+
#
|
287
|
+
# @private
|
288
|
+
def __vc_request
|
289
|
+
@__vc_request ||= controller.request if controller.respond_to?(:request)
|
290
|
+
end
|
300
291
|
|
292
|
+
# The content passed to the component instance as a block.
|
293
|
+
#
|
294
|
+
# @return [String]
|
301
295
|
def content
|
302
296
|
@__vc_content_evaluated = true
|
303
297
|
return @__vc_content if defined?(@__vc_content)
|
304
298
|
|
305
299
|
@__vc_content =
|
306
|
-
if
|
300
|
+
if __vc_render_in_block_provided?
|
307
301
|
view_context.capture(self, &@__vc_render_in_block)
|
308
|
-
elsif
|
302
|
+
elsif __vc_content_set_by_with_content_defined?
|
309
303
|
@__vc_content_set_by_with_content
|
310
304
|
end
|
311
305
|
end
|
312
306
|
|
307
|
+
# Whether `content` has been passed to the component.
|
308
|
+
#
|
309
|
+
# @return [Boolean]
|
310
|
+
def content?
|
311
|
+
__vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
|
312
|
+
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
attr_reader :view_context
|
317
|
+
|
318
|
+
def __vc_render_in_block_provided?
|
319
|
+
defined?(@view_context) && @view_context && @__vc_render_in_block
|
320
|
+
end
|
321
|
+
|
322
|
+
def __vc_content_set_by_with_content_defined?
|
323
|
+
defined?(@__vc_content_set_by_with_content)
|
324
|
+
end
|
325
|
+
|
313
326
|
def content_evaluated?
|
314
|
-
@__vc_content_evaluated
|
327
|
+
defined?(@__vc_content_evaluated) && @__vc_content_evaluated
|
315
328
|
end
|
316
329
|
|
317
330
|
def maybe_escape_html(text)
|
318
|
-
return text if
|
331
|
+
return text if __vc_request && !__vc_request.format.html?
|
319
332
|
return text if text.blank?
|
320
333
|
|
321
334
|
if text.html_safe?
|
@@ -326,13 +339,9 @@ module ViewComponent
|
|
326
339
|
end
|
327
340
|
end
|
328
341
|
|
329
|
-
def
|
330
|
-
|
331
|
-
|
332
|
-
else
|
333
|
-
maybe_escape_html(render_template_for(variant)) do
|
334
|
-
Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
|
335
|
-
end
|
342
|
+
def safe_output_preamble
|
343
|
+
maybe_escape_html(output_preamble) do
|
344
|
+
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.")
|
336
345
|
end
|
337
346
|
end
|
338
347
|
|
@@ -342,10 +351,6 @@ module ViewComponent
|
|
342
351
|
end
|
343
352
|
end
|
344
353
|
|
345
|
-
def compiler
|
346
|
-
@compiler ||= self.class.compiler
|
347
|
-
end
|
348
|
-
|
349
354
|
# Set the controller used for testing components:
|
350
355
|
#
|
351
356
|
# ```ruby
|
@@ -403,6 +408,14 @@ module ViewComponent
|
|
403
408
|
# config.view_component.generate.stimulus_controller = true
|
404
409
|
# ```
|
405
410
|
#
|
411
|
+
# #### `#typescript`
|
412
|
+
#
|
413
|
+
# Generate TypeScript files instead of JavaScript files:
|
414
|
+
#
|
415
|
+
# ```ruby
|
416
|
+
# config.view_component.generate.typescript = true
|
417
|
+
# ```
|
418
|
+
#
|
406
419
|
# #### #locale
|
407
420
|
#
|
408
421
|
# Always generate translations file alongside the component:
|
@@ -433,8 +446,16 @@ module ViewComponent
|
|
433
446
|
# Defaults to `false`.
|
434
447
|
|
435
448
|
class << self
|
449
|
+
# The file path of the component Ruby file.
|
450
|
+
#
|
451
|
+
# @return [String]
|
452
|
+
attr_reader :identifier
|
453
|
+
|
436
454
|
# @private
|
437
|
-
|
455
|
+
attr_writer :identifier
|
456
|
+
|
457
|
+
# @private
|
458
|
+
attr_accessor :virtual_path
|
438
459
|
|
439
460
|
# Find sidecar files for the given extensions.
|
440
461
|
#
|
@@ -444,13 +465,13 @@ module ViewComponent
|
|
444
465
|
# For example, one might collect sidecar CSS files that need to be compiled.
|
445
466
|
# @param extensions [Array<String>] Extensions of which to return matching sidecar files.
|
446
467
|
def sidecar_files(extensions)
|
447
|
-
return [] unless
|
468
|
+
return [] unless identifier
|
448
469
|
|
449
470
|
extensions = extensions.join(",")
|
450
471
|
|
451
472
|
# view files in a directory named like the component
|
452
|
-
directory = File.dirname(
|
453
|
-
filename = File.basename(
|
473
|
+
directory = File.dirname(identifier)
|
474
|
+
filename = File.basename(identifier, ".rb")
|
454
475
|
component_name = name.demodulize.underscore
|
455
476
|
|
456
477
|
# Add support for nested components defined in the same file.
|
@@ -475,7 +496,7 @@ module ViewComponent
|
|
475
496
|
|
476
497
|
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
|
477
498
|
|
478
|
-
(sidecar_files - [
|
499
|
+
(sidecar_files - [identifier] + sidecar_directory_files + nested_component_files).uniq
|
479
500
|
end
|
480
501
|
|
481
502
|
# Render a component for each element in a collection ([documentation](/guide/collections)):
|
@@ -485,16 +506,10 @@ module ViewComponent
|
|
485
506
|
# ```
|
486
507
|
#
|
487
508
|
# @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
|
509
|
+
# @param spacer_component [ViewComponent::Base] Component instance to be rendered between items.
|
488
510
|
# @param args [Arguments] Arguments to pass to the ViewComponent every time.
|
489
|
-
def with_collection(collection, **args)
|
490
|
-
Collection.new(self, collection, **args)
|
491
|
-
end
|
492
|
-
|
493
|
-
# Provide identifier for ActionView template annotations
|
494
|
-
#
|
495
|
-
# @private
|
496
|
-
def short_identifier
|
497
|
-
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
511
|
+
def with_collection(collection, spacer_component: nil, **args)
|
512
|
+
Collection.new(self, collection, spacer_component, **args)
|
498
513
|
end
|
499
514
|
|
500
515
|
# @private
|
@@ -509,12 +524,12 @@ module ViewComponent
|
|
509
524
|
# meaning it will not be called for any children and thus not compile their templates.
|
510
525
|
if !child.instance_methods(false).include?(:render_template_for) && !child.compiled?
|
511
526
|
child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
512
|
-
def render_template_for(variant = nil)
|
527
|
+
def render_template_for(variant = nil, format = nil)
|
513
528
|
# Force compilation here so the compiler always redefines render_template_for.
|
514
529
|
# This is mostly a safeguard to prevent infinite recursion.
|
515
530
|
self.class.compile(raise_errors: true, force: true)
|
516
531
|
# .compile replaces this method; call the new one
|
517
|
-
render_template_for(variant)
|
532
|
+
render_template_for(variant, format)
|
518
533
|
end
|
519
534
|
RUBY
|
520
535
|
end
|
@@ -528,16 +543,27 @@ module ViewComponent
|
|
528
543
|
# Derive the source location of the component Ruby file from the call stack.
|
529
544
|
# We need to ignore `inherited` frames here as they indicate that `inherited`
|
530
545
|
# has been re-defined by the consuming application, likely in ApplicationComponent.
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
)
|
546
|
+
# We use `base_label` method here instead of `label` to avoid cases where the method
|
547
|
+
# owner is included in a prefix like `ApplicationComponent.inherited`.
|
548
|
+
child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
|
549
|
+
|
550
|
+
# If Rails application is loaded, removes the first part of the path and the extension.
|
551
|
+
if defined?(Rails) && Rails.application
|
552
|
+
child.virtual_path = child.identifier.gsub(
|
553
|
+
/(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, ""
|
554
|
+
)
|
555
|
+
end
|
537
556
|
|
538
557
|
# Set collection parameter to the extended component
|
539
558
|
child.with_collection_parameter provided_collection_parameter
|
540
559
|
|
560
|
+
if instance_methods(false).include?(:render_template_for)
|
561
|
+
vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
|
562
|
+
|
563
|
+
vc_ancestor_calls.unshift(instance_method(:render_template_for))
|
564
|
+
child.instance_variable_set(:@__vc_ancestor_calls, vc_ancestor_calls)
|
565
|
+
end
|
566
|
+
|
541
567
|
super
|
542
568
|
end
|
543
569
|
|
@@ -546,10 +572,11 @@ module ViewComponent
|
|
546
572
|
compiler.compiled?
|
547
573
|
end
|
548
574
|
|
549
|
-
#
|
550
|
-
|
551
|
-
|
552
|
-
|
575
|
+
# @private
|
576
|
+
def ensure_compiled
|
577
|
+
compile unless compiled?
|
578
|
+
end
|
579
|
+
|
553
580
|
# @private
|
554
581
|
def compile(raise_errors: false, force: false)
|
555
582
|
compiler.compile(raise_errors: raise_errors, force: force)
|
@@ -560,22 +587,6 @@ module ViewComponent
|
|
560
587
|
@__vc_compiler ||= Compiler.new(self)
|
561
588
|
end
|
562
589
|
|
563
|
-
# we'll eventually want to update this to support other types
|
564
|
-
# @private
|
565
|
-
def type
|
566
|
-
"text/html"
|
567
|
-
end
|
568
|
-
|
569
|
-
# @private
|
570
|
-
def format
|
571
|
-
:html
|
572
|
-
end
|
573
|
-
|
574
|
-
# @private
|
575
|
-
def identifier
|
576
|
-
source_location
|
577
|
-
end
|
578
|
-
|
579
590
|
# Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
|
580
591
|
#
|
581
592
|
# ```ruby
|
@@ -585,6 +596,7 @@ module ViewComponent
|
|
585
596
|
# @param parameter [Symbol] The parameter name used when rendering elements of a collection.
|
586
597
|
def with_collection_parameter(parameter)
|
587
598
|
@provided_collection_parameter = parameter
|
599
|
+
@initialize_parameters = nil
|
588
600
|
end
|
589
601
|
|
590
602
|
# Strips trailing whitespace from templates before compiling them.
|
@@ -595,7 +607,7 @@ module ViewComponent
|
|
595
607
|
# end
|
596
608
|
# ```
|
597
609
|
#
|
598
|
-
# @param value [Boolean] Whether
|
610
|
+
# @param value [Boolean] Whether to strip newlines.
|
599
611
|
def strip_trailing_whitespace(value = true)
|
600
612
|
self.__vc_strip_trailing_whitespace = value
|
601
613
|
end
|
@@ -612,38 +624,31 @@ module ViewComponent
|
|
612
624
|
# validate that the default parameter name
|
613
625
|
# is accepted, as support for collection
|
614
626
|
# rendering is optional.
|
615
|
-
# @private
|
627
|
+
# @private
|
616
628
|
def validate_collection_parameter!(validate_default: false)
|
617
629
|
parameter = validate_default ? collection_parameter : provided_collection_parameter
|
618
630
|
|
619
631
|
return unless parameter
|
620
632
|
return if initialize_parameter_names.include?(parameter) || splatted_keyword_argument_present?
|
621
633
|
|
622
|
-
# If Ruby can't parse the component class, then the
|
634
|
+
# If Ruby can't parse the component class, then the initialize
|
623
635
|
# parameters will be empty and ViewComponent will not be able to render
|
624
636
|
# the component.
|
625
637
|
if initialize_parameters.empty?
|
626
|
-
raise
|
627
|
-
"It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
|
628
|
-
"To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
|
629
|
-
"See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
|
638
|
+
raise EmptyOrInvalidInitializerError.new(name, parameter)
|
630
639
|
end
|
631
640
|
|
632
|
-
raise
|
633
|
-
"which is required in order to render it as a collection.\n\n" \
|
634
|
-
"To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
|
635
|
-
"See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
|
641
|
+
raise MissingCollectionArgumentError.new(name, parameter)
|
636
642
|
end
|
637
643
|
|
638
644
|
# Ensure the component initializer doesn't define
|
639
645
|
# invalid parameters that could override the framework's
|
640
646
|
# methods.
|
641
|
-
# @private
|
647
|
+
# @private
|
642
648
|
def validate_initialization_parameters!
|
643
649
|
return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
|
644
650
|
|
645
|
-
raise
|
646
|
-
"public ViewComponent method. To fix this issue, rename the parameter."
|
651
|
+
raise ReservedParameterError.new(name, RESERVED_PARAMETER)
|
647
652
|
end
|
648
653
|
|
649
654
|
# @private
|
@@ -653,7 +658,7 @@ module ViewComponent
|
|
653
658
|
|
654
659
|
# @private
|
655
660
|
def collection_counter_parameter
|
656
|
-
"#{collection_parameter}_counter"
|
661
|
+
:"#{collection_parameter}_counter"
|
657
662
|
end
|
658
663
|
|
659
664
|
# @private
|
@@ -663,7 +668,7 @@ module ViewComponent
|
|
663
668
|
|
664
669
|
# @private
|
665
670
|
def collection_iteration_parameter
|
666
|
-
"#{collection_parameter}_iteration"
|
671
|
+
:"#{collection_parameter}_iteration"
|
667
672
|
end
|
668
673
|
|
669
674
|
# @private
|
@@ -687,7 +692,7 @@ module ViewComponent
|
|
687
692
|
end
|
688
693
|
|
689
694
|
def initialize_parameters
|
690
|
-
instance_method(:initialize).parameters
|
695
|
+
@initialize_parameters ||= instance_method(:initialize).parameters
|
691
696
|
end
|
692
697
|
|
693
698
|
def provided_collection_parameter
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
# CaptureCompatibility is a module that patches #capture to fix issues
|
5
|
+
# related to ViewComponent and functionality that relies on `capture`
|
6
|
+
# like forms, capture itself, turbo frames, etc.
|
7
|
+
#
|
8
|
+
# This underlying incompatibility with ViewComponent and capture is
|
9
|
+
# that several features like forms keep a reference to the primary
|
10
|
+
# `ActionView::Base` instance which has its own @output_buffer. When
|
11
|
+
# `#capture` is called on the original `ActionView::Base` instance while
|
12
|
+
# evaluating a block from a ViewComponent the @output_buffer is overridden
|
13
|
+
# in the ActionView::Base instance, and *not* the component. This results
|
14
|
+
# in a double render due to `#capture` implementation details.
|
15
|
+
#
|
16
|
+
# To resolve the issue, we override `#capture` so that we can delegate
|
17
|
+
# the `capture` logic to the ViewComponent that created the block.
|
18
|
+
module CaptureCompatibility
|
19
|
+
def self.included(base)
|
20
|
+
return if base < InstanceMethods
|
21
|
+
|
22
|
+
base.class_eval do
|
23
|
+
alias_method :original_capture, :capture
|
24
|
+
end
|
25
|
+
|
26
|
+
base.prepend(InstanceMethods)
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
def capture(*args, &block)
|
31
|
+
# Handle blocks that originate from C code and raise, such as `&:method`
|
32
|
+
return original_capture(*args, &block) if block.source_location.nil?
|
33
|
+
|
34
|
+
block_context = block.binding.receiver
|
35
|
+
|
36
|
+
if block_context != self && block_context.class < ActionView::Base
|
37
|
+
block_context.original_capture(*args, &block)
|
38
|
+
else
|
39
|
+
original_capture(*args, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|