view_component 2.49.1 → 3.23.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/LICENSE.txt +1 -1
- data/app/assets/vendor/prism.css +3 -195
- data/app/assets/vendor/prism.min.js +11 -11
- data/app/controllers/concerns/view_component/preview_actions.rb +108 -0
- data/app/controllers/view_components_controller.rb +1 -87
- data/app/controllers/view_components_system_test_controller.rb +30 -0
- data/app/helpers/preview_helper.rb +30 -12
- data/app/views/view_components/_preview_source.html.erb +3 -3
- data/app/views/view_components/preview.html.erb +2 -2
- data/docs/CHANGELOG.md +1653 -24
- data/lib/rails/generators/abstract_generator.rb +16 -10
- data/lib/rails/generators/component/component_generator.rb +8 -4
- 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 +4 -4
- data/lib/rails/generators/preview/component_generator.rb +17 -3
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +5 -1
- data/lib/rails/generators/rspec/component_generator.rb +15 -3
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +3 -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 +3 -1
- data/lib/view_component/base.rb +352 -196
- data/lib/view_component/capture_compatibility.rb +44 -0
- data/lib/view_component/collection.rb +28 -9
- data/lib/view_component/compiler.rb +162 -193
- data/lib/view_component/config.rb +225 -0
- data/lib/view_component/configurable.rb +17 -0
- data/lib/view_component/deprecation.rb +8 -0
- data/lib/view_component/engine.rb +74 -47
- 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 +21 -19
- data/lib/view_component/rails/tasks/view_component.rake +11 -2
- data/lib/view_component/render_component_helper.rb +1 -0
- data/lib/view_component/render_component_to_string_helper.rb +1 -1
- data/lib/view_component/render_to_string_monkey_patch.rb +1 -1
- data/lib/view_component/rendering_component_helper.rb +1 -1
- data/lib/view_component/rendering_monkey_patch.rb +1 -1
- data/lib/view_component/slot.rb +119 -1
- data/lib/view_component/slotable.rb +393 -96
- data/lib/view_component/slotable_default.rb +20 -0
- data/lib/view_component/system_test_case.rb +13 -0
- data/lib/view_component/system_test_helpers.rb +27 -0
- data/lib/view_component/template.rb +134 -0
- data/lib/view_component/test_helpers.rb +208 -47
- data/lib/view_component/translatable.rb +51 -33
- data/lib/view_component/use_helpers.rb +42 -0
- data/lib/view_component/version.rb +5 -4
- data/lib/view_component/with_content_helper.rb +3 -8
- data/lib/view_component.rb +7 -12
- metadata +339 -57
- data/lib/rails/generators/component/USAGE +0 -13
- data/lib/view_component/content_areas.rb +0 -57
- data/lib/view_component/polymorphic_slots.rb +0 -73
- data/lib/view_component/preview_template_error.rb +0 -6
- data/lib/view_component/previewable.rb +0 -62
- data/lib/view_component/slot_v2.rb +0 -104
- data/lib/view_component/slotable_v2.rb +0 -307
- data/lib/view_component/template_error.rb +0 -9
- data/lib/yard/mattr_accessor_handler.rb +0 -19
data/lib/view_component/base.rb
CHANGED
@@ -4,40 +4,68 @@ require "action_view"
|
|
4
4
|
require "active_support/configurable"
|
5
5
|
require "view_component/collection"
|
6
6
|
require "view_component/compile_cache"
|
7
|
-
require "view_component/
|
8
|
-
require "view_component/
|
9
|
-
require "view_component/
|
7
|
+
require "view_component/compiler"
|
8
|
+
require "view_component/config"
|
9
|
+
require "view_component/errors"
|
10
|
+
require "view_component/inline_template"
|
11
|
+
require "view_component/preview"
|
10
12
|
require "view_component/slotable"
|
11
|
-
require "view_component/
|
13
|
+
require "view_component/slotable_default"
|
14
|
+
require "view_component/template"
|
15
|
+
require "view_component/translatable"
|
12
16
|
require "view_component/with_content_helper"
|
17
|
+
require "view_component/use_helpers"
|
13
18
|
|
14
19
|
module ViewComponent
|
15
20
|
class Base < ActionView::Base
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
class << self
|
22
|
+
delegate(*ViewComponent::Config.defaults.keys, to: :config)
|
23
|
+
|
24
|
+
# Returns the current config.
|
25
|
+
#
|
26
|
+
# @return [ActiveSupport::OrderedOptions]
|
27
|
+
def config
|
28
|
+
module_parents.each do |module_parent|
|
29
|
+
next unless module_parent.respond_to?(:config)
|
30
|
+
module_parent_config = module_parent.config.try(:view_component)
|
31
|
+
return module_parent_config if module_parent_config
|
32
|
+
end
|
33
|
+
ViewComponent::Config.current
|
34
|
+
end
|
35
|
+
end
|
21
36
|
|
22
|
-
|
37
|
+
include ViewComponent::InlineTemplate
|
38
|
+
include ViewComponent::UseHelpers
|
39
|
+
include ViewComponent::Slotable
|
40
|
+
include ViewComponent::Translatable
|
41
|
+
include ViewComponent::WithContentHelper
|
23
42
|
|
24
43
|
RESERVED_PARAMETER = :content
|
44
|
+
VC_INTERNAL_DEFAULT_FORMAT = :html
|
25
45
|
|
26
46
|
# For CSRF authenticity tokens in forms
|
27
47
|
delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
|
28
48
|
|
29
|
-
|
30
|
-
|
49
|
+
# For Content Security Policy nonces
|
50
|
+
delegate :content_security_policy_nonce, to: :helpers
|
51
|
+
|
52
|
+
# 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
|
31
55
|
|
32
56
|
attr_accessor :__vc_original_view_context
|
33
57
|
|
34
|
-
#
|
35
|
-
#
|
58
|
+
# Components render in their own view context. Helpers and other functionality
|
59
|
+
# require a reference to the original Rails view context, an instance of
|
60
|
+
# `ActionView::Base`. Use this method to set a reference to the original
|
61
|
+
# view context. Objects that implement this method will render in the component's
|
62
|
+
# view context, while objects that don't will render in the original view context
|
63
|
+
# so helpers, etc work as expected.
|
36
64
|
#
|
37
|
-
#
|
38
|
-
# @
|
39
|
-
def
|
40
|
-
|
65
|
+
# @param view_context [ActionView::Base] The original view context.
|
66
|
+
# @return [void]
|
67
|
+
def set_original_view_context(view_context)
|
68
|
+
self.__vc_original_view_context = view_context
|
41
69
|
end
|
42
70
|
|
43
71
|
# Entrypoint for rendering components.
|
@@ -54,6 +82,8 @@ module ViewComponent
|
|
54
82
|
@view_context = view_context
|
55
83
|
self.__vc_original_view_context ||= view_context
|
56
84
|
|
85
|
+
@output_buffer = ActionView::OutputBuffer.new
|
86
|
+
|
57
87
|
@lookup_context ||= view_context.lookup_context
|
58
88
|
|
59
89
|
# required for path helpers in older Rails versions
|
@@ -74,11 +104,7 @@ module ViewComponent
|
|
74
104
|
@current_template = self
|
75
105
|
|
76
106
|
if block && defined?(@__vc_content_set_by_with_content)
|
77
|
-
raise
|
78
|
-
"It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
|
79
|
-
"which means that ViewComponent doesn't know which content to use.\n\n" \
|
80
|
-
"To fix this issue, use either `with_content` or a block."
|
81
|
-
)
|
107
|
+
raise DuplicateContentError.new(self.class.name)
|
82
108
|
end
|
83
109
|
|
84
110
|
@__vc_content_evaluated = false
|
@@ -87,7 +113,14 @@ module ViewComponent
|
|
87
113
|
before_render
|
88
114
|
|
89
115
|
if render?
|
90
|
-
render_template_for(@__vc_variant).to_s
|
116
|
+
rendered_template = render_template_for(@__vc_variant, __vc_request&.format&.to_sym).to_s
|
117
|
+
|
118
|
+
# Avoid allocating new string when output_preamble and output_postamble are blank
|
119
|
+
if output_preamble.blank? && output_postamble.blank?
|
120
|
+
rendered_template
|
121
|
+
else
|
122
|
+
safe_output_preamble + rendered_template + safe_output_postamble
|
123
|
+
end
|
91
124
|
else
|
92
125
|
""
|
93
126
|
end
|
@@ -95,26 +128,64 @@ module ViewComponent
|
|
95
128
|
@current_template = old_current_template
|
96
129
|
end
|
97
130
|
|
98
|
-
#
|
131
|
+
# Subclass components that call `super` inside their template code will cause a
|
132
|
+
# double render if they emit the result.
|
133
|
+
#
|
134
|
+
# ```erb
|
135
|
+
# <%= super %> # double-renders
|
136
|
+
# <% super %> # doesn't double-render
|
137
|
+
# ```
|
138
|
+
#
|
139
|
+
# `super` also doesn't consider the current variant. `render_parent` renders the
|
140
|
+
# parent template considering the current variant and emits the result without
|
141
|
+
# double-rendering.
|
142
|
+
def render_parent
|
143
|
+
render_parent_to_string
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
# Renders the parent component to a string and returns it. This method is meant
|
148
|
+
# to be used inside custom #call methods when a string result is desired, eg.
|
149
|
+
#
|
150
|
+
# ```ruby
|
151
|
+
# def call
|
152
|
+
# "<div>#{render_parent_to_string}</div>"
|
153
|
+
# end
|
154
|
+
# ```
|
155
|
+
#
|
156
|
+
# When rendering the parent inside an .erb template, use `#render_parent` instead.
|
157
|
+
def render_parent_to_string
|
158
|
+
@__vc_parent_render_level ||= 0 # ensure a good starting value
|
159
|
+
|
160
|
+
begin
|
161
|
+
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
|
162
|
+
@__vc_parent_render_level += 1
|
163
|
+
|
164
|
+
target_render.bind_call(self, @__vc_variant)
|
165
|
+
ensure
|
166
|
+
@__vc_parent_render_level -= 1
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Optional content to be returned before the rendered template.
|
99
171
|
#
|
100
172
|
# @return [String]
|
101
|
-
def
|
102
|
-
""
|
173
|
+
def output_preamble
|
174
|
+
@@default_output_preamble ||= "".html_safe
|
103
175
|
end
|
104
176
|
|
105
|
-
#
|
106
|
-
# depend on having access to the view context, such as helpers.
|
177
|
+
# Optional content to be returned after the rendered template.
|
107
178
|
#
|
108
|
-
# @return [
|
109
|
-
def
|
110
|
-
|
179
|
+
# @return [String]
|
180
|
+
def output_postamble
|
181
|
+
@@default_output_postamble ||= "".html_safe
|
111
182
|
end
|
112
183
|
|
113
|
-
# Called
|
184
|
+
# Called before rendering the component. Override to perform operations that
|
185
|
+
# depend on having access to the view context, such as helpers.
|
114
186
|
#
|
115
|
-
# @deprecated Use `#before_render` instead. Will be removed in v3.0.0.
|
116
187
|
# @return [void]
|
117
|
-
def
|
188
|
+
def before_render
|
118
189
|
# noop
|
119
190
|
end
|
120
191
|
|
@@ -125,8 +196,11 @@ module ViewComponent
|
|
125
196
|
true
|
126
197
|
end
|
127
198
|
|
199
|
+
# Override the ActionView::Base initializer so that components
|
200
|
+
# do not need to define their own initializers.
|
128
201
|
# @private
|
129
|
-
def initialize(*)
|
202
|
+
def initialize(*)
|
203
|
+
end
|
130
204
|
|
131
205
|
# Re-use original view_context if we're not rendering a component.
|
132
206
|
#
|
@@ -136,8 +210,8 @@ module ViewComponent
|
|
136
210
|
#
|
137
211
|
# @private
|
138
212
|
def render(options = {}, args = {}, &block)
|
139
|
-
if options.
|
140
|
-
options.__vc_original_view_context
|
213
|
+
if options.respond_to?(:set_original_view_context)
|
214
|
+
options.set_original_view_context(self.__vc_original_view_context)
|
141
215
|
super
|
142
216
|
else
|
143
217
|
__vc_original_view_context.render(options, args, &block)
|
@@ -149,16 +223,7 @@ module ViewComponent
|
|
149
223
|
#
|
150
224
|
# @return [ActionController::Base]
|
151
225
|
def controller
|
152
|
-
if view_context.nil?
|
153
|
-
raise(
|
154
|
-
ViewContextCalledBeforeRenderError,
|
155
|
-
"`#controller` can't be used during initialization, as it depends " \
|
156
|
-
"on the view context that only exists once a ViewComponent is passed to " \
|
157
|
-
"the Rails render pipeline.\n\n" \
|
158
|
-
"It's sometimes possible to fix this issue by moving code dependent on " \
|
159
|
-
"`#controller` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
|
160
|
-
)
|
161
|
-
end
|
226
|
+
raise ControllerCalledBeforeRenderError if view_context.nil?
|
162
227
|
|
163
228
|
@__vc_controller ||= view_context.controller
|
164
229
|
end
|
@@ -168,16 +233,7 @@ module ViewComponent
|
|
168
233
|
#
|
169
234
|
# @return [ActionView::Base]
|
170
235
|
def helpers
|
171
|
-
if view_context.nil?
|
172
|
-
raise(
|
173
|
-
ViewContextCalledBeforeRenderError,
|
174
|
-
"`#helpers` can't be used during initialization, as it depends " \
|
175
|
-
"on the view context that only exists once a ViewComponent is passed to " \
|
176
|
-
"the Rails render pipeline.\n\n" \
|
177
|
-
"It's sometimes possible to fix this issue by moving code dependent on " \
|
178
|
-
"`#helpers` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
|
179
|
-
)
|
180
|
-
end
|
236
|
+
raise HelpersCalledBeforeRenderError if view_context.nil?
|
181
237
|
|
182
238
|
# Attempt to re-use the original view_context passed to the first
|
183
239
|
# component rendered in the rendering pipeline. This prevents the
|
@@ -189,6 +245,22 @@ module ViewComponent
|
|
189
245
|
@__vc_helpers ||= __vc_original_view_context || controller.view_context
|
190
246
|
end
|
191
247
|
|
248
|
+
if ::Rails.env.development? || ::Rails.env.test?
|
249
|
+
# @private
|
250
|
+
def method_missing(method_name, *args) # rubocop:disable Style/MissingRespondToMissing
|
251
|
+
super
|
252
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
253
|
+
e.set_backtrace e.backtrace.tap(&:shift)
|
254
|
+
raise e, <<~MESSAGE.chomp if view_context && e.is_a?(NameError) && helpers.respond_to?(method_name)
|
255
|
+
#{e.message}
|
256
|
+
|
257
|
+
You may be trying to call a method provided as a view helper. Did you mean `helpers.#{method_name}`?
|
258
|
+
MESSAGE
|
259
|
+
|
260
|
+
raise
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
192
264
|
# Exposes .virtual_path as an instance method
|
193
265
|
#
|
194
266
|
# @private
|
@@ -206,25 +278,7 @@ module ViewComponent
|
|
206
278
|
#
|
207
279
|
# @private
|
208
280
|
def format
|
209
|
-
|
210
|
-
if defined?(@__vc_variant)
|
211
|
-
@__vc_variant
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
# Use the provided variant instead of the one determined by the current request.
|
216
|
-
#
|
217
|
-
# @deprecated Will be removed in v3.0.0.
|
218
|
-
# @param variant [Symbol] The variant to be used by the component.
|
219
|
-
# @return [self]
|
220
|
-
def with_variant(variant)
|
221
|
-
ActiveSupport::Deprecation.warn(
|
222
|
-
"`with_variant` is deprecated and will be removed in ViewComponent v3.0.0."
|
223
|
-
)
|
224
|
-
|
225
|
-
@__vc_variant = variant
|
226
|
-
|
227
|
-
self
|
281
|
+
@__vc_variant if defined?(@__vc_variant)
|
228
282
|
end
|
229
283
|
|
230
284
|
# The current request. Use sparingly as doing so introduces coupling that
|
@@ -232,116 +286,199 @@ module ViewComponent
|
|
232
286
|
#
|
233
287
|
# @return [ActionDispatch::Request]
|
234
288
|
def request
|
235
|
-
|
289
|
+
__vc_request
|
236
290
|
end
|
237
291
|
|
238
|
-
|
239
|
-
|
240
|
-
|
292
|
+
# Enables consumers to override request/@request
|
293
|
+
#
|
294
|
+
# @private
|
295
|
+
def __vc_request
|
296
|
+
@__vc_request ||= controller.request if controller.respond_to?(:request)
|
297
|
+
end
|
241
298
|
|
299
|
+
# The content passed to the component instance as a block.
|
300
|
+
#
|
301
|
+
# @return [String]
|
242
302
|
def content
|
243
303
|
@__vc_content_evaluated = true
|
244
304
|
return @__vc_content if defined?(@__vc_content)
|
245
305
|
|
246
306
|
@__vc_content =
|
247
|
-
if
|
307
|
+
if __vc_render_in_block_provided?
|
248
308
|
view_context.capture(self, &@__vc_render_in_block)
|
249
|
-
elsif
|
309
|
+
elsif __vc_content_set_by_with_content_defined?
|
250
310
|
@__vc_content_set_by_with_content
|
251
311
|
end
|
252
312
|
end
|
253
313
|
|
314
|
+
# Whether `content` has been passed to the component.
|
315
|
+
#
|
316
|
+
# @return [Boolean]
|
317
|
+
def content?
|
318
|
+
__vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
|
319
|
+
end
|
320
|
+
|
321
|
+
private
|
322
|
+
|
323
|
+
attr_reader :view_context
|
324
|
+
|
325
|
+
def __vc_render_in_block_provided?
|
326
|
+
defined?(@view_context) && @view_context && @__vc_render_in_block
|
327
|
+
end
|
328
|
+
|
329
|
+
def __vc_content_set_by_with_content_defined?
|
330
|
+
defined?(@__vc_content_set_by_with_content)
|
331
|
+
end
|
332
|
+
|
254
333
|
def content_evaluated?
|
255
|
-
@__vc_content_evaluated
|
334
|
+
defined?(@__vc_content_evaluated) && @__vc_content_evaluated
|
335
|
+
end
|
336
|
+
|
337
|
+
def maybe_escape_html(text)
|
338
|
+
return text if __vc_request && !__vc_request.format.html?
|
339
|
+
return text if text.blank?
|
340
|
+
|
341
|
+
if text.html_safe?
|
342
|
+
text
|
343
|
+
else
|
344
|
+
yield
|
345
|
+
html_escape(text)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def safe_output_preamble
|
350
|
+
maybe_escape_html(output_preamble) do
|
351
|
+
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
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def safe_output_postamble
|
356
|
+
maybe_escape_html(output_postamble) do
|
357
|
+
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
|
+
end
|
256
359
|
end
|
257
360
|
|
258
361
|
# Set the controller used for testing components:
|
259
362
|
#
|
260
|
-
#
|
363
|
+
# ```ruby
|
364
|
+
# config.view_component.test_controller = "MyTestController"
|
365
|
+
# ```
|
261
366
|
#
|
262
|
-
# Defaults to ApplicationController. Can also be
|
263
|
-
# basis using `with_controller_class`.
|
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`.
|
264
369
|
#
|
265
|
-
mattr_accessor :test_controller
|
266
|
-
@@test_controller = "ApplicationController"
|
267
370
|
|
268
371
|
# Set if render monkey patches should be included or not in Rails <6.1:
|
269
372
|
#
|
270
|
-
#
|
373
|
+
# ```ruby
|
374
|
+
# config.view_component.render_monkey_patch_enabled = false
|
375
|
+
# ```
|
271
376
|
#
|
272
|
-
mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
|
273
377
|
|
274
|
-
#
|
378
|
+
# Path for component files
|
275
379
|
#
|
276
|
-
#
|
380
|
+
# ```ruby
|
381
|
+
# config.view_component.view_component_path = "app/my_components"
|
382
|
+
# ```
|
277
383
|
#
|
278
|
-
# Defaults to `
|
384
|
+
# Defaults to `nil`. If this is falsy, `app/components` is used.
|
279
385
|
#
|
280
|
-
mattr_accessor :generate_stimulus_controller, instance_writer: false, default: false
|
281
386
|
|
282
|
-
#
|
387
|
+
# Parent class for generated components
|
283
388
|
#
|
284
|
-
#
|
389
|
+
# ```ruby
|
390
|
+
# config.view_component.component_parent_class = "MyBaseComponent"
|
391
|
+
# ```
|
285
392
|
#
|
286
|
-
# Defaults to
|
393
|
+
# Defaults to nil. If this is falsy, generators will use
|
394
|
+
# "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
|
287
395
|
#
|
288
|
-
mattr_accessor :generate_locale, instance_writer: false, default: false
|
289
396
|
|
290
|
-
#
|
397
|
+
# Configuration for generators.
|
291
398
|
#
|
292
|
-
#
|
399
|
+
# All options under this namespace default to `false` unless otherwise
|
400
|
+
# stated.
|
293
401
|
#
|
294
|
-
#
|
402
|
+
# #### #sidecar
|
295
403
|
#
|
296
|
-
#
|
297
|
-
# Fallback on `[:en]` when no available_locales is defined.
|
404
|
+
# Always generate a component with a sidecar directory:
|
298
405
|
#
|
299
|
-
|
300
|
-
|
301
|
-
#
|
406
|
+
# ```ruby
|
407
|
+
# config.view_component.generate.sidecar = true
|
408
|
+
# ```
|
302
409
|
#
|
303
|
-
#
|
410
|
+
# #### #stimulus_controller
|
304
411
|
#
|
305
|
-
#
|
412
|
+
# Always generate a Stimulus controller alongside the component:
|
306
413
|
#
|
307
|
-
|
308
|
-
|
309
|
-
#
|
414
|
+
# ```ruby
|
415
|
+
# config.view_component.generate.stimulus_controller = true
|
416
|
+
# ```
|
310
417
|
#
|
311
|
-
#
|
418
|
+
# #### `#typescript`
|
312
419
|
#
|
313
|
-
#
|
420
|
+
# Generate TypeScript files instead of JavaScript files:
|
314
421
|
#
|
315
|
-
|
316
|
-
|
317
|
-
#
|
422
|
+
# ```ruby
|
423
|
+
# config.view_component.generate.typescript = true
|
424
|
+
# ```
|
425
|
+
#
|
426
|
+
# #### #locale
|
427
|
+
#
|
428
|
+
# Always generate translations file alongside the component:
|
429
|
+
#
|
430
|
+
# ```ruby
|
431
|
+
# config.view_component.generate.locale = true
|
432
|
+
# ```
|
433
|
+
#
|
434
|
+
# #### #distinct_locale_files
|
435
|
+
#
|
436
|
+
# Always generate as many translations files as available locales:
|
318
437
|
#
|
319
|
-
#
|
438
|
+
# ```ruby
|
439
|
+
# config.view_component.generate.distinct_locale_files = true
|
440
|
+
# ```
|
320
441
|
#
|
321
|
-
#
|
442
|
+
# One file will be generated for each configured `I18n.available_locales`,
|
443
|
+
# falling back to `[:en]` when no `available_locales` is defined.
|
322
444
|
#
|
323
|
-
|
445
|
+
# #### #preview
|
446
|
+
#
|
447
|
+
# Always generate preview alongside the component:
|
448
|
+
#
|
449
|
+
# ```ruby
|
450
|
+
# config.view_component.generate.preview = true
|
451
|
+
# ```
|
452
|
+
#
|
453
|
+
# Defaults to `false`.
|
324
454
|
|
325
455
|
class << self
|
456
|
+
# The file path of the component Ruby file.
|
457
|
+
#
|
458
|
+
# @return [String]
|
459
|
+
attr_reader :identifier
|
460
|
+
|
326
461
|
# @private
|
327
|
-
|
462
|
+
attr_writer :identifier
|
463
|
+
|
464
|
+
# @private
|
465
|
+
attr_accessor :virtual_path
|
328
466
|
|
329
|
-
# EXPERIMENTAL: This API is experimental and may be removed at any time.
|
330
467
|
# Find sidecar files for the given extensions.
|
331
468
|
#
|
332
469
|
# The provided array of extensions is expected to contain
|
333
|
-
# strings starting without the
|
470
|
+
# strings starting without the dot, example: `["erb", "haml"]`.
|
334
471
|
#
|
335
472
|
# For example, one might collect sidecar CSS files that need to be compiled.
|
336
|
-
# @
|
337
|
-
def
|
338
|
-
return [] unless
|
473
|
+
# @param extensions [Array<String>] Extensions of which to return matching sidecar files.
|
474
|
+
def sidecar_files(extensions)
|
475
|
+
return [] unless identifier
|
339
476
|
|
340
477
|
extensions = extensions.join(",")
|
341
478
|
|
342
479
|
# view files in a directory named like the component
|
343
|
-
directory = File.dirname(
|
344
|
-
filename = File.basename(
|
480
|
+
directory = File.dirname(identifier)
|
481
|
+
filename = File.basename(identifier, ".rb")
|
345
482
|
component_name = name.demodulize.underscore
|
346
483
|
|
347
484
|
# Add support for nested components defined in the same file.
|
@@ -366,24 +503,20 @@ module ViewComponent
|
|
366
503
|
|
367
504
|
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
|
368
505
|
|
369
|
-
(sidecar_files - [
|
506
|
+
(sidecar_files - [identifier] + sidecar_directory_files + nested_component_files).uniq
|
370
507
|
end
|
371
508
|
|
372
509
|
# Render a component for each element in a collection ([documentation](/guide/collections)):
|
373
510
|
#
|
374
|
-
#
|
511
|
+
# ```ruby
|
512
|
+
# render(ProductsComponent.with_collection(@products, foo: :bar))
|
513
|
+
# ```
|
375
514
|
#
|
376
515
|
# @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
|
516
|
+
# @param spacer_component [ViewComponent::Base] Component instance to be rendered between items.
|
377
517
|
# @param args [Arguments] Arguments to pass to the ViewComponent every time.
|
378
|
-
def with_collection(collection, **args)
|
379
|
-
Collection.new(self, collection, **args)
|
380
|
-
end
|
381
|
-
|
382
|
-
# Provide identifier for ActionView template annotations
|
383
|
-
#
|
384
|
-
# @private
|
385
|
-
def short_identifier
|
386
|
-
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
518
|
+
def with_collection(collection, spacer_component: nil, **args)
|
519
|
+
Collection.new(self, collection, spacer_component, **args)
|
387
520
|
end
|
388
521
|
|
389
522
|
# @private
|
@@ -392,25 +525,52 @@ module ViewComponent
|
|
392
525
|
# `compile` defines
|
393
526
|
compile
|
394
527
|
|
528
|
+
# Give the child its own personal #render_template_for to protect against the case when
|
529
|
+
# eager loading is disabled and the parent component is rendered before the child. In
|
530
|
+
# such a scenario, the parent will override ViewComponent::Base#render_template_for,
|
531
|
+
# 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.compiled?
|
533
|
+
child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
534
|
+
def render_template_for(variant = nil, format = nil)
|
535
|
+
# Force compilation here so the compiler always redefines render_template_for.
|
536
|
+
# This is mostly a safeguard to prevent infinite recursion.
|
537
|
+
self.class.compile(raise_errors: true, force: true)
|
538
|
+
# .compile replaces this method; call the new one
|
539
|
+
render_template_for(variant, format)
|
540
|
+
end
|
541
|
+
RUBY
|
542
|
+
end
|
543
|
+
|
395
544
|
# If Rails application is loaded, add application url_helpers to the component context
|
396
545
|
# we need to check this to use this gem as a dependency
|
397
|
-
if defined?(Rails) && Rails.application
|
398
|
-
child.include Rails.application.routes.url_helpers
|
546
|
+
if defined?(Rails) && Rails.application && !(child < Rails.application.routes.url_helpers)
|
547
|
+
child.include Rails.application.routes.url_helpers
|
399
548
|
end
|
400
549
|
|
401
550
|
# Derive the source location of the component Ruby file from the call stack.
|
402
551
|
# We need to ignore `inherited` frames here as they indicate that `inherited`
|
403
552
|
# has been re-defined by the consuming application, likely in ApplicationComponent.
|
404
|
-
|
553
|
+
# We use `base_label` method here instead of `label` to avoid cases where the method
|
554
|
+
# owner is included in a prefix like `ApplicationComponent.inherited`.
|
555
|
+
child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
|
405
556
|
|
406
|
-
#
|
407
|
-
|
408
|
-
|
409
|
-
|
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
|
410
563
|
|
411
564
|
# Set collection parameter to the extended component
|
412
565
|
child.with_collection_parameter provided_collection_parameter
|
413
566
|
|
567
|
+
if instance_methods(false).include?(:render_template_for)
|
568
|
+
vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
|
569
|
+
|
570
|
+
vc_ancestor_calls.unshift(instance_method(:render_template_for))
|
571
|
+
child.instance_variable_set(:@__vc_ancestor_calls, vc_ancestor_calls)
|
572
|
+
end
|
573
|
+
|
414
574
|
super
|
415
575
|
end
|
416
576
|
|
@@ -419,43 +579,51 @@ module ViewComponent
|
|
419
579
|
compiler.compiled?
|
420
580
|
end
|
421
581
|
|
422
|
-
# Compile templates to instance methods, assuming they haven't been compiled already.
|
423
|
-
#
|
424
|
-
# Do as much work as possible in this step, as doing so reduces the amount
|
425
|
-
# of work done each time a component is rendered.
|
426
|
-
# @private
|
427
|
-
def compile(raise_errors: false)
|
428
|
-
compiler.compile(raise_errors: raise_errors)
|
429
|
-
end
|
430
|
-
|
431
|
-
# @private
|
432
|
-
def compiler
|
433
|
-
@__vc_compiler ||= Compiler.new(self)
|
434
|
-
end
|
435
|
-
|
436
|
-
# we'll eventually want to update this to support other types
|
437
582
|
# @private
|
438
|
-
def
|
439
|
-
|
583
|
+
def ensure_compiled
|
584
|
+
compile unless compiled?
|
440
585
|
end
|
441
586
|
|
442
587
|
# @private
|
443
|
-
def
|
444
|
-
:
|
588
|
+
def compile(raise_errors: false, force: false)
|
589
|
+
compiler.compile(raise_errors: raise_errors, force: force)
|
445
590
|
end
|
446
591
|
|
447
592
|
# @private
|
448
|
-
def
|
449
|
-
|
593
|
+
def compiler
|
594
|
+
@__vc_compiler ||= Compiler.new(self)
|
450
595
|
end
|
451
596
|
|
452
597
|
# Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
|
453
598
|
#
|
454
|
-
#
|
599
|
+
# ```ruby
|
600
|
+
# with_collection_parameter :item
|
601
|
+
# ```
|
455
602
|
#
|
456
603
|
# @param parameter [Symbol] The parameter name used when rendering elements of a collection.
|
457
604
|
def with_collection_parameter(parameter)
|
458
605
|
@provided_collection_parameter = parameter
|
606
|
+
@initialize_parameters = nil
|
607
|
+
end
|
608
|
+
|
609
|
+
# Strips trailing whitespace from templates before compiling them.
|
610
|
+
#
|
611
|
+
# ```ruby
|
612
|
+
# class MyComponent < ViewComponent::Base
|
613
|
+
# strip_trailing_whitespace
|
614
|
+
# end
|
615
|
+
# ```
|
616
|
+
#
|
617
|
+
# @param value [Boolean] Whether to strip newlines.
|
618
|
+
def strip_trailing_whitespace(value = true)
|
619
|
+
self.__vc_strip_trailing_whitespace = value
|
620
|
+
end
|
621
|
+
|
622
|
+
# Whether trailing whitespace will be stripped before compilation.
|
623
|
+
#
|
624
|
+
# @return [Boolean]
|
625
|
+
def strip_trailing_whitespace?
|
626
|
+
__vc_strip_trailing_whitespace
|
459
627
|
end
|
460
628
|
|
461
629
|
# Ensure the component initializer accepts the
|
@@ -463,58 +631,41 @@ module ViewComponent
|
|
463
631
|
# validate that the default parameter name
|
464
632
|
# is accepted, as support for collection
|
465
633
|
# rendering is optional.
|
466
|
-
# @private
|
634
|
+
# @private
|
467
635
|
def validate_collection_parameter!(validate_default: false)
|
468
636
|
parameter = validate_default ? collection_parameter : provided_collection_parameter
|
469
637
|
|
470
638
|
return unless parameter
|
471
|
-
return if initialize_parameter_names.include?(parameter)
|
639
|
+
return if initialize_parameter_names.include?(parameter) || splatted_keyword_argument_present?
|
472
640
|
|
473
|
-
# If Ruby can't parse the component class, then the
|
641
|
+
# If Ruby can't parse the component class, then the initialize
|
474
642
|
# parameters will be empty and ViewComponent will not be able to render
|
475
643
|
# the component.
|
476
644
|
if initialize_parameters.empty?
|
477
|
-
raise
|
478
|
-
"The #{self} initializer is empty or invalid." \
|
479
|
-
"It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
|
480
|
-
"To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
|
481
|
-
"See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
|
482
|
-
)
|
645
|
+
raise EmptyOrInvalidInitializerError.new(name, parameter)
|
483
646
|
end
|
484
647
|
|
485
|
-
raise
|
486
|
-
"The initializer for #{self} doesn't accept the parameter `#{parameter}`, " \
|
487
|
-
"which is required in order to render it as a collection.\n\n" \
|
488
|
-
"To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
|
489
|
-
"See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
|
490
|
-
)
|
648
|
+
raise MissingCollectionArgumentError.new(name, parameter)
|
491
649
|
end
|
492
650
|
|
493
651
|
# Ensure the component initializer doesn't define
|
494
652
|
# invalid parameters that could override the framework's
|
495
653
|
# methods.
|
496
|
-
# @private
|
654
|
+
# @private
|
497
655
|
def validate_initialization_parameters!
|
498
656
|
return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
|
499
657
|
|
500
|
-
raise
|
501
|
-
"#{self} initializer can't accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
|
502
|
-
"public ViewComponent method. To fix this issue, rename the parameter."
|
503
|
-
)
|
658
|
+
raise ReservedParameterError.new(name, RESERVED_PARAMETER)
|
504
659
|
end
|
505
660
|
|
506
661
|
# @private
|
507
662
|
def collection_parameter
|
508
|
-
|
509
|
-
provided_collection_parameter
|
510
|
-
else
|
511
|
-
name && name.demodulize.underscore.chomp("_component").to_sym
|
512
|
-
end
|
663
|
+
provided_collection_parameter || name && name.demodulize.underscore.chomp("_component").to_sym
|
513
664
|
end
|
514
665
|
|
515
666
|
# @private
|
516
667
|
def collection_counter_parameter
|
517
|
-
"#{collection_parameter}_counter"
|
668
|
+
:"#{collection_parameter}_counter"
|
518
669
|
end
|
519
670
|
|
520
671
|
# @private
|
@@ -524,7 +675,7 @@ module ViewComponent
|
|
524
675
|
|
525
676
|
# @private
|
526
677
|
def collection_iteration_parameter
|
527
|
-
"#{collection_parameter}_iteration"
|
678
|
+
:"#{collection_parameter}_iteration"
|
528
679
|
end
|
529
680
|
|
530
681
|
# @private
|
@@ -534,6 +685,11 @@ module ViewComponent
|
|
534
685
|
|
535
686
|
private
|
536
687
|
|
688
|
+
def splatted_keyword_argument_present?
|
689
|
+
initialize_parameters.flatten.include?(:keyrest) &&
|
690
|
+
!initialize_parameters.include?([:keyrest, :**]) # Un-named splatted keyword args don't count!
|
691
|
+
end
|
692
|
+
|
537
693
|
def initialize_parameter_names
|
538
694
|
return attribute_names.map(&:to_sym) if respond_to?(:attribute_names)
|
539
695
|
|
@@ -543,7 +699,7 @@ module ViewComponent
|
|
543
699
|
end
|
544
700
|
|
545
701
|
def initialize_parameters
|
546
|
-
instance_method(:initialize).parameters
|
702
|
+
@initialize_parameters ||= instance_method(:initialize).parameters
|
547
703
|
end
|
548
704
|
|
549
705
|
def provided_collection_parameter
|