view_component 3.23.2 → 4.12.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 +11 -14
- data/app/controllers/view_components_system_test_controller.rb +25 -19
- 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 +612 -108
- data/lib/{rails/generators → generators/view_component}/abstract_generator.rb +2 -2
- data/lib/{rails/generators → generators/view_component}/component/component_generator.rb +17 -4
- 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 +252 -163
- data/lib/view_component/collection.rb +33 -35
- data/lib/view_component/compiler.rb +55 -80
- data/lib/view_component/config.rb +55 -85
- data/lib/view_component/configurable.rb +19 -3
- data/lib/view_component/deprecation.rb +1 -1
- data/lib/view_component/engine.rb +46 -106
- data/lib/view_component/errors.rb +16 -34
- data/lib/view_component/inline_template.rb +4 -4
- data/lib/view_component/instrumentation.rb +21 -14
- data/lib/view_component/preview.rb +7 -12
- data/lib/view_component/request_details.rb +30 -0
- data/lib/view_component/slot.rb +7 -15
- data/lib/view_component/slotable.rb +94 -80
- data/lib/view_component/system_spec_helpers.rb +11 -0
- data/lib/view_component/system_test_helpers.rb +2 -2
- data/lib/view_component/template.rb +159 -83
- data/lib/view_component/test_helpers.rb +67 -50
- data/lib/view_component/translatable.rb +33 -32
- data/lib/view_component/version.rb +3 -3
- data/lib/view_component.rb +9 -6
- metadata +36 -556
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "action_view"
|
|
4
|
-
require "
|
|
4
|
+
require "action_view/base"
|
|
5
5
|
require "view_component/collection"
|
|
6
6
|
require "view_component/compile_cache"
|
|
7
7
|
require "view_component/compiler"
|
|
@@ -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.application.routes)
|
|
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
|
|
@@ -63,9 +82,17 @@ module ViewComponent
|
|
|
63
82
|
# so helpers, etc work as expected.
|
|
64
83
|
#
|
|
65
84
|
# @param view_context [ActionView::Base] The original view context.
|
|
85
|
+
#
|
|
66
86
|
# @return [void]
|
|
67
87
|
def set_original_view_context(view_context)
|
|
68
|
-
|
|
88
|
+
# noop
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
using RequestDetails
|
|
92
|
+
|
|
93
|
+
# Including `Rails.application.routes.url_helpers` defines an initializer that accepts (...),
|
|
94
|
+
# so we have to define our own empty initializer to overwrite it.
|
|
95
|
+
def initialize
|
|
69
96
|
end
|
|
70
97
|
|
|
71
98
|
# Entrypoint for rendering components.
|
|
@@ -76,55 +103,82 @@ module ViewComponent
|
|
|
76
103
|
# Returns HTML that has been escaped by the respective template handler.
|
|
77
104
|
#
|
|
78
105
|
# @return [String]
|
|
79
|
-
def render_in(view_context, &block)
|
|
80
|
-
self.class.
|
|
106
|
+
def render_in(view_context, **_, &block)
|
|
107
|
+
self.class.__vc_compile(raise_errors: true)
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
self.__vc_original_view_context ||= view_context
|
|
109
|
+
__vc_reset_render_state!
|
|
84
110
|
|
|
85
|
-
@
|
|
111
|
+
@view_context = view_context
|
|
112
|
+
@old_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
|
113
|
+
self.__vc_original_view_context = view_context
|
|
86
114
|
|
|
87
|
-
@
|
|
115
|
+
@output_buffer = view_context.output_buffer
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
@view_renderer ||= view_context.view_renderer
|
|
117
|
+
@lookup_context = view_context.lookup_context
|
|
91
118
|
|
|
92
119
|
# For content_for
|
|
93
|
-
@view_flow
|
|
120
|
+
@view_flow = view_context.view_flow
|
|
94
121
|
|
|
95
122
|
# For i18n
|
|
96
123
|
@virtual_path ||= virtual_path
|
|
97
124
|
|
|
98
|
-
#
|
|
99
|
-
@
|
|
125
|
+
# Describes the inferred request constraints (locales, formats, variants)
|
|
126
|
+
@__vc_requested_details = @lookup_context.vc_requested_details
|
|
100
127
|
|
|
101
128
|
# For caching, such as #cache_if
|
|
102
129
|
@current_template = nil unless defined?(@current_template)
|
|
103
130
|
old_current_template = @current_template
|
|
104
|
-
@current_template = self
|
|
105
131
|
|
|
106
132
|
if block && defined?(@__vc_content_set_by_with_content)
|
|
107
133
|
raise DuplicateContentError.new(self.class.name)
|
|
108
134
|
end
|
|
109
135
|
|
|
110
136
|
@__vc_content_evaluated = false
|
|
137
|
+
remove_instance_variable(:@__vc_content) if defined?(@__vc_content)
|
|
111
138
|
@__vc_render_in_block = block
|
|
139
|
+
@view_context.instance_variable_set(:@virtual_path, virtual_path)
|
|
112
140
|
|
|
113
141
|
before_render
|
|
114
142
|
|
|
115
143
|
if render?
|
|
116
|
-
|
|
144
|
+
value = nil
|
|
145
|
+
|
|
146
|
+
@output_buffer.with_buffer do
|
|
147
|
+
inner_rendered_template = nil
|
|
148
|
+
around_rendered_template = around_render do
|
|
149
|
+
inner_rendered_template = render_template_for(@__vc_requested_details).to_s
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# If `around_render` returned the same object the block yielded, the inner
|
|
153
|
+
# template's escaping is authoritative and we can trust the result. If the
|
|
154
|
+
# user replaced/wrapped the value, re-check HTML safety to prevent
|
|
155
|
+
# bypassing the escaping applied to normal `#call` return values
|
|
156
|
+
# (GHSA-97jw-64cj-jc58).
|
|
157
|
+
rendered_template = if around_rendered_template.equal?(inner_rendered_template)
|
|
158
|
+
around_rendered_template
|
|
159
|
+
else
|
|
160
|
+
__vc_safe_around_render_output(around_rendered_template)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Avoid allocating new string when output_preamble and output_postamble are blank
|
|
164
|
+
value = if output_preamble.blank? && output_postamble.blank?
|
|
165
|
+
rendered_template
|
|
166
|
+
else
|
|
167
|
+
__vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble
|
|
168
|
+
end
|
|
169
|
+
end
|
|
117
170
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else
|
|
122
|
-
safe_output_preamble + rendered_template + safe_output_postamble
|
|
171
|
+
if ActionView::Base.annotate_rendered_view_with_filenames && current_template.inline_call? && request&.format == :html
|
|
172
|
+
identifier = defined?(Rails.root) ? self.class.identifier.sub("#{Rails.root}/", "") : self.class.identifier
|
|
173
|
+
value = "<!-- BEGIN #{identifier} -->".html_safe + value + "<!-- END #{identifier} -->".html_safe
|
|
123
174
|
end
|
|
175
|
+
|
|
176
|
+
value
|
|
124
177
|
else
|
|
125
|
-
""
|
|
178
|
+
"".html_safe
|
|
126
179
|
end
|
|
127
180
|
ensure
|
|
181
|
+
view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
|
|
128
182
|
@current_template = old_current_template
|
|
129
183
|
end
|
|
130
184
|
|
|
@@ -161,7 +215,7 @@ module ViewComponent
|
|
|
161
215
|
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
|
|
162
216
|
@__vc_parent_render_level += 1
|
|
163
217
|
|
|
164
|
-
target_render.bind_call(self, @
|
|
218
|
+
target_render.bind_call(self, @__vc_requested_details)
|
|
165
219
|
ensure
|
|
166
220
|
@__vc_parent_render_level -= 1
|
|
167
221
|
end
|
|
@@ -189,6 +243,14 @@ module ViewComponent
|
|
|
189
243
|
# noop
|
|
190
244
|
end
|
|
191
245
|
|
|
246
|
+
# Called around rendering the component. Override to wrap the rendering of a
|
|
247
|
+
# component in custom instrumentation, etc.
|
|
248
|
+
#
|
|
249
|
+
# @return [void]
|
|
250
|
+
def around_render
|
|
251
|
+
yield
|
|
252
|
+
end
|
|
253
|
+
|
|
192
254
|
# Override to determine whether the ViewComponent should render.
|
|
193
255
|
#
|
|
194
256
|
# @return [Boolean]
|
|
@@ -196,28 +258,56 @@ module ViewComponent
|
|
|
196
258
|
true
|
|
197
259
|
end
|
|
198
260
|
|
|
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
261
|
# Re-use original view_context if we're not rendering a component.
|
|
206
262
|
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
#
|
|
263
|
+
# As of v4, ViewComponent::Base re-uses the existing view context created
|
|
264
|
+
# by ActionView, meaning the current view context and the original view
|
|
265
|
+
# context are the same object. set_original_view_context is still called
|
|
266
|
+
# to maintain backwards compatibility.
|
|
210
267
|
#
|
|
211
268
|
# @private
|
|
212
269
|
def render(options = {}, args = {}, &block)
|
|
213
270
|
if options.respond_to?(:set_original_view_context)
|
|
214
|
-
options.set_original_view_context(
|
|
215
|
-
|
|
271
|
+
options.set_original_view_context(__vc_original_view_context)
|
|
272
|
+
|
|
273
|
+
# We assume options is a component, so there's no need to evaluate the
|
|
274
|
+
# block in the view context as we do below.
|
|
275
|
+
@view_context.render(options, args, &block)
|
|
276
|
+
elsif block
|
|
277
|
+
__vc_original_view_context.render(options, args) do
|
|
278
|
+
# capture the block output in the view context of the component
|
|
279
|
+
output = capture(&block)
|
|
280
|
+
|
|
281
|
+
# Partials are rendered to their own buffer and do not append to the
|
|
282
|
+
# original @output_buffer we retain a reference to in #render_in. This
|
|
283
|
+
# is a problem since the block passed to us here in the #render method
|
|
284
|
+
# is evaluated within the context of ViewComponent::Base, and thus
|
|
285
|
+
# appends to the original @output_buffer. To avoid this, we evaluate the
|
|
286
|
+
# block in the view context instead, which will append to the output buffer
|
|
287
|
+
# created for the partial.
|
|
288
|
+
__vc_original_view_context.capture { output }
|
|
289
|
+
end
|
|
216
290
|
else
|
|
217
|
-
__vc_original_view_context.render(options, args
|
|
291
|
+
__vc_original_view_context.render(options, args)
|
|
218
292
|
end
|
|
219
293
|
end
|
|
220
294
|
|
|
295
|
+
# Sync @output_buffer with the view context's current output buffer before
|
|
296
|
+
# capturing. Form helpers create builders with @template_object pointing to
|
|
297
|
+
# the component. When those builders later call capture inside a partial
|
|
298
|
+
# (whose _run allocated a fresh OutputBuffer on the view context), the
|
|
299
|
+
# component's stale @output_buffer would capture from the wrong buffer.
|
|
300
|
+
# Temporarily switching to the view context's buffer keeps both in sync.
|
|
301
|
+
#
|
|
302
|
+
# @private
|
|
303
|
+
def capture(...)
|
|
304
|
+
old_output_buffer = @output_buffer
|
|
305
|
+
@output_buffer = view_context.output_buffer if view_context
|
|
306
|
+
super
|
|
307
|
+
ensure
|
|
308
|
+
@output_buffer = old_output_buffer
|
|
309
|
+
end
|
|
310
|
+
|
|
221
311
|
# The current controller. Use sparingly as doing so introduces coupling
|
|
222
312
|
# that inhibits encapsulation & reuse, often making testing difficult.
|
|
223
313
|
#
|
|
@@ -245,7 +335,7 @@ module ViewComponent
|
|
|
245
335
|
@__vc_helpers ||= __vc_original_view_context || controller.view_context
|
|
246
336
|
end
|
|
247
337
|
|
|
248
|
-
if ::Rails.env.development? || ::Rails.env.test?
|
|
338
|
+
if defined?(Rails.env) && (::Rails.env.development? || ::Rails.env.test?)
|
|
249
339
|
# @private
|
|
250
340
|
def method_missing(method_name, *args) # rubocop:disable Style/MissingRespondToMissing
|
|
251
341
|
super
|
|
@@ -274,11 +364,12 @@ module ViewComponent
|
|
|
274
364
|
[]
|
|
275
365
|
end
|
|
276
366
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
367
|
+
if defined?(Rails::VERSION) && Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
|
368
|
+
# Rails expects us to define `format` on all renderables,
|
|
369
|
+
# but we do not know the `format` of a ViewComponent until runtime.
|
|
370
|
+
def format
|
|
371
|
+
nil
|
|
372
|
+
end
|
|
282
373
|
end
|
|
283
374
|
|
|
284
375
|
# The current request. Use sparingly as doing so introduces coupling that
|
|
@@ -289,10 +380,9 @@ module ViewComponent
|
|
|
289
380
|
__vc_request
|
|
290
381
|
end
|
|
291
382
|
|
|
292
|
-
# Enables consumers to override request/@request
|
|
293
|
-
#
|
|
294
383
|
# @private
|
|
295
384
|
def __vc_request
|
|
385
|
+
# The current request (if present, as mailers/jobs/etc do not have a request)
|
|
296
386
|
@__vc_request ||= controller.request if controller.respond_to?(:request)
|
|
297
387
|
end
|
|
298
388
|
|
|
@@ -305,7 +395,9 @@ module ViewComponent
|
|
|
305
395
|
|
|
306
396
|
@__vc_content =
|
|
307
397
|
if __vc_render_in_block_provided?
|
|
308
|
-
|
|
398
|
+
with_captured_virtual_path(@old_virtual_path) do
|
|
399
|
+
view_context.capture(self, &@__vc_render_in_block)
|
|
400
|
+
end
|
|
309
401
|
elsif __vc_content_set_by_with_content_defined?
|
|
310
402
|
@__vc_content_set_by_with_content
|
|
311
403
|
end
|
|
@@ -318,6 +410,17 @@ module ViewComponent
|
|
|
318
410
|
__vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
|
|
319
411
|
end
|
|
320
412
|
|
|
413
|
+
# @private
|
|
414
|
+
# Temporarily sets the virtual path to the captured value, then restores it.
|
|
415
|
+
# This ensures translations and other path-dependent code execute with the correct scope.
|
|
416
|
+
def with_captured_virtual_path(captured_path)
|
|
417
|
+
old_virtual_path = @view_context.instance_variable_get(:@virtual_path)
|
|
418
|
+
@view_context.instance_variable_set(:@virtual_path, captured_path)
|
|
419
|
+
yield
|
|
420
|
+
ensure
|
|
421
|
+
@view_context.instance_variable_set(:@virtual_path, old_virtual_path)
|
|
422
|
+
end
|
|
423
|
+
|
|
321
424
|
private
|
|
322
425
|
|
|
323
426
|
attr_reader :view_context
|
|
@@ -330,12 +433,8 @@ module ViewComponent
|
|
|
330
433
|
defined?(@__vc_content_set_by_with_content)
|
|
331
434
|
end
|
|
332
435
|
|
|
333
|
-
def
|
|
334
|
-
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def maybe_escape_html(text)
|
|
338
|
-
return text if __vc_request && !__vc_request.format.html?
|
|
436
|
+
def __vc_maybe_escape_html(text)
|
|
437
|
+
return text if @current_template && !@current_template.html?
|
|
339
438
|
return text if text.blank?
|
|
340
439
|
|
|
341
440
|
if text.html_safe?
|
|
@@ -346,53 +445,39 @@ module ViewComponent
|
|
|
346
445
|
end
|
|
347
446
|
end
|
|
348
447
|
|
|
349
|
-
def
|
|
350
|
-
|
|
448
|
+
def __vc_safe_output_preamble
|
|
449
|
+
__vc_maybe_escape_html(output_preamble) do
|
|
351
450
|
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
451
|
end
|
|
353
452
|
end
|
|
354
453
|
|
|
355
|
-
def
|
|
356
|
-
|
|
454
|
+
def __vc_safe_output_postamble
|
|
455
|
+
__vc_maybe_escape_html(output_postamble) do
|
|
357
456
|
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
457
|
end
|
|
359
458
|
end
|
|
360
459
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
#
|
|
460
|
+
def __vc_safe_around_render_output(output)
|
|
461
|
+
__vc_maybe_escape_html(output) do
|
|
462
|
+
Kernel.warn("WARNING: The #{self.class} component's around_render returned an HTML-unsafe string. The output will be automatically escaped, but you may want to investigate.")
|
|
463
|
+
end
|
|
464
|
+
end
|
|
386
465
|
|
|
387
|
-
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
466
|
+
# Resets every render-scoped instance variable derived from the calling view
|
|
467
|
+
# context so a reused instance cannot leak controller/helper/request/format
|
|
468
|
+
# state from a previous render. Slot state (`@__vc_set_slots`,
|
|
469
|
+
# `@__vc_content_set_by_with_content`) is intentionally preserved because it
|
|
470
|
+
# is populated by callers _before_ `render_in` runs (e.g. via `with_*`
|
|
471
|
+
# slot setters or `with_content`).
|
|
472
|
+
def __vc_reset_render_state!
|
|
473
|
+
%i[
|
|
474
|
+
@__vc_controller
|
|
475
|
+
@__vc_helpers
|
|
476
|
+
@__vc_request
|
|
477
|
+
].each do |ivar|
|
|
478
|
+
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
396
481
|
|
|
397
482
|
# Configuration for generators.
|
|
398
483
|
#
|
|
@@ -451,6 +536,18 @@ module ViewComponent
|
|
|
451
536
|
# ```
|
|
452
537
|
#
|
|
453
538
|
# Defaults to `false`.
|
|
539
|
+
#
|
|
540
|
+
# #### ßparent_class
|
|
541
|
+
#
|
|
542
|
+
# Parent class for generated components
|
|
543
|
+
#
|
|
544
|
+
# ```ruby
|
|
545
|
+
# config.view_component.generate.parent_class = "MyBaseComponent"
|
|
546
|
+
# ```
|
|
547
|
+
#
|
|
548
|
+
# Defaults to nil. If this is falsy, generators will use
|
|
549
|
+
# "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
|
|
550
|
+
#
|
|
454
551
|
|
|
455
552
|
class << self
|
|
456
553
|
# The file path of the component Ruby file.
|
|
@@ -473,6 +570,7 @@ module ViewComponent
|
|
|
473
570
|
# @param extensions [Array<String>] Extensions of which to return matching sidecar files.
|
|
474
571
|
def sidecar_files(extensions)
|
|
475
572
|
return [] unless identifier
|
|
573
|
+
return [] unless name
|
|
476
574
|
|
|
477
575
|
extensions = extensions.join(",")
|
|
478
576
|
|
|
@@ -519,52 +617,45 @@ module ViewComponent
|
|
|
519
617
|
Collection.new(self, collection, spacer_component, **args)
|
|
520
618
|
end
|
|
521
619
|
|
|
620
|
+
# @private
|
|
621
|
+
def __vc_compile(raise_errors: false, force: false)
|
|
622
|
+
__vc_compiler.compile(raise_errors: raise_errors, force: force)
|
|
623
|
+
end
|
|
624
|
+
|
|
522
625
|
# @private
|
|
523
626
|
def inherited(child)
|
|
524
627
|
# Compile so child will inherit compiled `call_*` template methods that
|
|
525
628
|
# `compile` defines
|
|
526
|
-
|
|
629
|
+
__vc_compile
|
|
527
630
|
|
|
528
631
|
# Give the child its own personal #render_template_for to protect against the case when
|
|
529
632
|
# eager loading is disabled and the parent component is rendered before the child. In
|
|
530
633
|
# such a scenario, the parent will override ViewComponent::Base#render_template_for,
|
|
531
634
|
# meaning it will not be called for any children and thus not compile their templates.
|
|
532
|
-
if !child.
|
|
635
|
+
if !child.method_defined?(:render_template_for, false) && !child.__vc_compiled?
|
|
533
636
|
child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
534
|
-
def render_template_for(
|
|
637
|
+
def render_template_for(requested_details)
|
|
535
638
|
# Force compilation here so the compiler always redefines render_template_for.
|
|
536
639
|
# This is mostly a safeguard to prevent infinite recursion.
|
|
537
|
-
self.class.
|
|
538
|
-
# .
|
|
539
|
-
render_template_for(
|
|
640
|
+
self.class.__vc_compile(raise_errors: true, force: true)
|
|
641
|
+
# .__vc_compile replaces this method; call the new one
|
|
642
|
+
render_template_for(requested_details)
|
|
540
643
|
end
|
|
541
644
|
RUBY
|
|
542
645
|
end
|
|
543
646
|
|
|
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
647
|
# Derive the source location of the component Ruby file from the call stack.
|
|
551
648
|
# We need to ignore `inherited` frames here as they indicate that `inherited`
|
|
552
649
|
# has been re-defined by the consuming application, likely in ApplicationComponent.
|
|
553
650
|
# We use `base_label` method here instead of `label` to avoid cases where the method
|
|
554
651
|
# owner is included in a prefix like `ApplicationComponent.inherited`.
|
|
555
652
|
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
|
|
653
|
+
child.virtual_path = child.name&.underscore
|
|
563
654
|
|
|
564
655
|
# Set collection parameter to the extended component
|
|
565
|
-
child.with_collection_parameter
|
|
656
|
+
child.with_collection_parameter(__vc_provided_collection_parameter)
|
|
566
657
|
|
|
567
|
-
if
|
|
658
|
+
if method_defined?(:render_template_for, false)
|
|
568
659
|
vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
|
|
569
660
|
|
|
570
661
|
vc_ancestor_calls.unshift(instance_method(:render_template_for))
|
|
@@ -575,22 +666,27 @@ module ViewComponent
|
|
|
575
666
|
end
|
|
576
667
|
|
|
577
668
|
# @private
|
|
578
|
-
def
|
|
579
|
-
|
|
669
|
+
def __vc_compiled?
|
|
670
|
+
__vc_compiler.compiled?
|
|
580
671
|
end
|
|
581
672
|
|
|
582
|
-
#
|
|
583
|
-
|
|
584
|
-
|
|
673
|
+
# Hook called by the compiler after a component is compiled.
|
|
674
|
+
#
|
|
675
|
+
# Extensions can override this class method to run logic after
|
|
676
|
+
# compilation (e.g., generate helpers, register metadata, etc.).
|
|
677
|
+
#
|
|
678
|
+
# By default, this is a no-op.
|
|
679
|
+
def after_compile
|
|
680
|
+
# no-op by default
|
|
585
681
|
end
|
|
586
682
|
|
|
587
683
|
# @private
|
|
588
|
-
def
|
|
589
|
-
|
|
684
|
+
def __vc_ensure_compiled
|
|
685
|
+
__vc_compile unless __vc_compiled?
|
|
590
686
|
end
|
|
591
687
|
|
|
592
688
|
# @private
|
|
593
|
-
def
|
|
689
|
+
def __vc_compiler
|
|
594
690
|
@__vc_compiler ||= Compiler.new(self)
|
|
595
691
|
end
|
|
596
692
|
|
|
@@ -602,8 +698,8 @@ module ViewComponent
|
|
|
602
698
|
#
|
|
603
699
|
# @param parameter [Symbol] The parameter name used when rendering elements of a collection.
|
|
604
700
|
def with_collection_parameter(parameter)
|
|
605
|
-
@
|
|
606
|
-
@
|
|
701
|
+
@__vc_provided_collection_parameter = parameter
|
|
702
|
+
@__vc_initialize_parameters = nil
|
|
607
703
|
end
|
|
608
704
|
|
|
609
705
|
# Strips trailing whitespace from templates before compiling them.
|
|
@@ -632,18 +728,11 @@ module ViewComponent
|
|
|
632
728
|
# is accepted, as support for collection
|
|
633
729
|
# rendering is optional.
|
|
634
730
|
# @private
|
|
635
|
-
def
|
|
636
|
-
parameter = validate_default ?
|
|
731
|
+
def __vc_validate_collection_parameter!(validate_default: false)
|
|
732
|
+
parameter = validate_default ? __vc_collection_parameter : __vc_provided_collection_parameter
|
|
637
733
|
|
|
638
734
|
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
|
|
735
|
+
return if __vc_initialize_parameter_names.include?(parameter) || __vc_splatted_keyword_argument_present?
|
|
647
736
|
|
|
648
737
|
raise MissingCollectionArgumentError.new(name, parameter)
|
|
649
738
|
end
|
|
@@ -652,58 +741,58 @@ module ViewComponent
|
|
|
652
741
|
# invalid parameters that could override the framework's
|
|
653
742
|
# methods.
|
|
654
743
|
# @private
|
|
655
|
-
def
|
|
656
|
-
return unless
|
|
744
|
+
def __vc_validate_initialization_parameters!
|
|
745
|
+
return unless __vc_initialize_parameter_names.include?(:content)
|
|
657
746
|
|
|
658
|
-
raise ReservedParameterError.new(name,
|
|
747
|
+
raise ReservedParameterError.new(name, :content)
|
|
659
748
|
end
|
|
660
749
|
|
|
661
750
|
# @private
|
|
662
|
-
def
|
|
663
|
-
|
|
751
|
+
def __vc_collection_parameter
|
|
752
|
+
@__vc_provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym
|
|
664
753
|
end
|
|
665
754
|
|
|
666
755
|
# @private
|
|
667
|
-
def
|
|
668
|
-
:"#{
|
|
756
|
+
def __vc_collection_counter_parameter
|
|
757
|
+
@__vc_collection_counter_parameter ||= :"#{__vc_collection_parameter}_counter"
|
|
669
758
|
end
|
|
670
759
|
|
|
671
760
|
# @private
|
|
672
|
-
def
|
|
673
|
-
|
|
761
|
+
def __vc_counter_argument_present?
|
|
762
|
+
__vc_initialize_parameter_names.include?(__vc_collection_counter_parameter)
|
|
674
763
|
end
|
|
675
764
|
|
|
676
765
|
# @private
|
|
677
|
-
def
|
|
678
|
-
:"#{
|
|
766
|
+
def __vc_collection_iteration_parameter
|
|
767
|
+
@__vc_collection_iteration_parameter ||= :"#{__vc_collection_parameter}_iteration"
|
|
679
768
|
end
|
|
680
769
|
|
|
681
770
|
# @private
|
|
682
|
-
def
|
|
683
|
-
|
|
771
|
+
def __vc_iteration_argument_present?
|
|
772
|
+
__vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter)
|
|
684
773
|
end
|
|
685
774
|
|
|
686
775
|
private
|
|
687
776
|
|
|
688
|
-
def
|
|
689
|
-
|
|
690
|
-
!initialize_parameters.include?([:keyrest, :**]) # Un-named splatted keyword args don't count!
|
|
777
|
+
def __vc_splatted_keyword_argument_present?
|
|
778
|
+
__vc_initialize_parameters.flatten.include?(:keyrest)
|
|
691
779
|
end
|
|
692
780
|
|
|
693
|
-
def
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
781
|
+
def __vc_initialize_parameter_names
|
|
782
|
+
@__vc_initialize_parameter_names ||=
|
|
783
|
+
if respond_to?(:attribute_names)
|
|
784
|
+
attribute_names.map(&:to_sym)
|
|
785
|
+
else
|
|
786
|
+
__vc_initialize_parameters.map(&:last)
|
|
787
|
+
end
|
|
699
788
|
end
|
|
700
789
|
|
|
701
|
-
def
|
|
702
|
-
@
|
|
790
|
+
def __vc_initialize_parameters
|
|
791
|
+
@__vc_initialize_parameters ||= instance_method(:initialize).parameters
|
|
703
792
|
end
|
|
704
793
|
|
|
705
|
-
def
|
|
706
|
-
@
|
|
794
|
+
def __vc_provided_collection_parameter
|
|
795
|
+
@__vc_provided_collection_parameter ||= nil
|
|
707
796
|
end
|
|
708
797
|
end
|
|
709
798
|
|