view_component 4.0.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/view_components_system_test_controller.rb +12 -1
- data/docs/CHANGELOG.md +190 -108
- data/lib/generators/view_component/component/component_generator.rb +1 -1
- data/lib/view_component/base.rb +92 -23
- data/lib/view_component/collection.rb +17 -13
- data/lib/view_component/compiler.rb +12 -10
- data/lib/view_component/config.rb +5 -1
- data/lib/view_component/configurable.rb +18 -2
- data/lib/view_component/engine.rb +12 -2
- data/lib/view_component/inline_template.rb +1 -0
- data/lib/view_component/instrumentation.rb +19 -6
- data/lib/view_component/preview.rb +3 -1
- data/lib/view_component/slot.rb +3 -4
- data/lib/view_component/slotable.rb +12 -3
- data/lib/view_component/system_test_helpers.rb +1 -0
- data/lib/view_component/template.rb +55 -2
- data/lib/view_component/test_helpers.rb +25 -11
- data/lib/view_component/version.rb +2 -2
- data/lib/view_component.rb +2 -1
- metadata +15 -7
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"
|
|
@@ -47,7 +47,7 @@ module ViewComponent
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
include ActionView::Helpers
|
|
50
|
-
include Rails.application.routes.url_helpers if defined?(Rails
|
|
50
|
+
include Rails.application.routes.url_helpers if defined?(Rails.application.routes)
|
|
51
51
|
include ERB::Escape
|
|
52
52
|
include ActiveSupport::CoreExt::ERBUtil
|
|
53
53
|
|
|
@@ -82,6 +82,7 @@ module ViewComponent
|
|
|
82
82
|
# so helpers, etc work as expected.
|
|
83
83
|
#
|
|
84
84
|
# @param view_context [ActionView::Base] The original view context.
|
|
85
|
+
#
|
|
85
86
|
# @return [void]
|
|
86
87
|
def set_original_view_context(view_context)
|
|
87
88
|
# noop
|
|
@@ -102,25 +103,27 @@ module ViewComponent
|
|
|
102
103
|
# Returns HTML that has been escaped by the respective template handler.
|
|
103
104
|
#
|
|
104
105
|
# @return [String]
|
|
105
|
-
def render_in(view_context, &block)
|
|
106
|
+
def render_in(view_context, **_, &block)
|
|
106
107
|
self.class.__vc_compile(raise_errors: true)
|
|
107
108
|
|
|
109
|
+
__vc_reset_render_state!
|
|
110
|
+
|
|
108
111
|
@view_context = view_context
|
|
109
112
|
@old_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
|
110
|
-
self.__vc_original_view_context
|
|
113
|
+
self.__vc_original_view_context = view_context
|
|
111
114
|
|
|
112
115
|
@output_buffer = view_context.output_buffer
|
|
113
116
|
|
|
114
|
-
@lookup_context
|
|
117
|
+
@lookup_context = view_context.lookup_context
|
|
115
118
|
|
|
116
119
|
# For content_for
|
|
117
|
-
@view_flow
|
|
120
|
+
@view_flow = view_context.view_flow
|
|
118
121
|
|
|
119
122
|
# For i18n
|
|
120
123
|
@virtual_path ||= virtual_path
|
|
121
124
|
|
|
122
125
|
# Describes the inferred request constraints (locales, formats, variants)
|
|
123
|
-
@__vc_requested_details
|
|
126
|
+
@__vc_requested_details = @lookup_context.vc_requested_details
|
|
124
127
|
|
|
125
128
|
# For caching, such as #cache_if
|
|
126
129
|
@current_template = nil unless defined?(@current_template)
|
|
@@ -131,7 +134,9 @@ module ViewComponent
|
|
|
131
134
|
end
|
|
132
135
|
|
|
133
136
|
@__vc_content_evaluated = false
|
|
137
|
+
remove_instance_variable(:@__vc_content) if defined?(@__vc_content)
|
|
134
138
|
@__vc_render_in_block = block
|
|
139
|
+
@view_context.instance_variable_set(:@virtual_path, virtual_path)
|
|
135
140
|
|
|
136
141
|
before_render
|
|
137
142
|
|
|
@@ -139,12 +144,21 @@ module ViewComponent
|
|
|
139
144
|
value = nil
|
|
140
145
|
|
|
141
146
|
@output_buffer.with_buffer do
|
|
142
|
-
|
|
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
|
|
143
151
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
148
162
|
|
|
149
163
|
# Avoid allocating new string when output_preamble and output_postamble are blank
|
|
150
164
|
value = if output_preamble.blank? && output_postamble.blank?
|
|
@@ -161,7 +175,7 @@ module ViewComponent
|
|
|
161
175
|
|
|
162
176
|
value
|
|
163
177
|
else
|
|
164
|
-
""
|
|
178
|
+
"".html_safe
|
|
165
179
|
end
|
|
166
180
|
ensure
|
|
167
181
|
view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
|
|
@@ -254,13 +268,16 @@ module ViewComponent
|
|
|
254
268
|
# @private
|
|
255
269
|
def render(options = {}, args = {}, &block)
|
|
256
270
|
if options.respond_to?(:set_original_view_context)
|
|
257
|
-
options.set_original_view_context(
|
|
271
|
+
options.set_original_view_context(__vc_original_view_context)
|
|
258
272
|
|
|
259
273
|
# We assume options is a component, so there's no need to evaluate the
|
|
260
274
|
# block in the view context as we do below.
|
|
261
275
|
@view_context.render(options, args, &block)
|
|
262
276
|
elsif block
|
|
263
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
|
+
|
|
264
281
|
# Partials are rendered to their own buffer and do not append to the
|
|
265
282
|
# original @output_buffer we retain a reference to in #render_in. This
|
|
266
283
|
# is a problem since the block passed to us here in the #render method
|
|
@@ -268,13 +285,29 @@ module ViewComponent
|
|
|
268
285
|
# appends to the original @output_buffer. To avoid this, we evaluate the
|
|
269
286
|
# block in the view context instead, which will append to the output buffer
|
|
270
287
|
# created for the partial.
|
|
271
|
-
__vc_original_view_context.
|
|
288
|
+
__vc_original_view_context.capture { output }
|
|
272
289
|
end
|
|
273
290
|
else
|
|
274
291
|
__vc_original_view_context.render(options, args)
|
|
275
292
|
end
|
|
276
293
|
end
|
|
277
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
|
+
|
|
278
311
|
# The current controller. Use sparingly as doing so introduces coupling
|
|
279
312
|
# that inhibits encapsulation & reuse, often making testing difficult.
|
|
280
313
|
#
|
|
@@ -302,7 +335,7 @@ module ViewComponent
|
|
|
302
335
|
@__vc_helpers ||= __vc_original_view_context || controller.view_context
|
|
303
336
|
end
|
|
304
337
|
|
|
305
|
-
if ::Rails.env.development? || ::Rails.env.test?
|
|
338
|
+
if defined?(Rails.env) && (::Rails.env.development? || ::Rails.env.test?)
|
|
306
339
|
# @private
|
|
307
340
|
def method_missing(method_name, *args) # rubocop:disable Style/MissingRespondToMissing
|
|
308
341
|
super
|
|
@@ -331,7 +364,7 @@ module ViewComponent
|
|
|
331
364
|
[]
|
|
332
365
|
end
|
|
333
366
|
|
|
334
|
-
if Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
|
367
|
+
if defined?(Rails::VERSION) && Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
|
335
368
|
# Rails expects us to define `format` on all renderables,
|
|
336
369
|
# but we do not know the `format` of a ViewComponent until runtime.
|
|
337
370
|
def format
|
|
@@ -362,7 +395,7 @@ module ViewComponent
|
|
|
362
395
|
|
|
363
396
|
@__vc_content =
|
|
364
397
|
if __vc_render_in_block_provided?
|
|
365
|
-
|
|
398
|
+
with_captured_virtual_path(@old_virtual_path) do
|
|
366
399
|
view_context.capture(self, &@__vc_render_in_block)
|
|
367
400
|
end
|
|
368
401
|
elsif __vc_content_set_by_with_content_defined?
|
|
@@ -378,11 +411,14 @@ module ViewComponent
|
|
|
378
411
|
end
|
|
379
412
|
|
|
380
413
|
# @private
|
|
381
|
-
|
|
382
|
-
|
|
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)
|
|
383
419
|
yield
|
|
384
420
|
ensure
|
|
385
|
-
@view_context.instance_variable_set(:@virtual_path,
|
|
421
|
+
@view_context.instance_variable_set(:@virtual_path, old_virtual_path)
|
|
386
422
|
end
|
|
387
423
|
|
|
388
424
|
private
|
|
@@ -421,6 +457,28 @@ module ViewComponent
|
|
|
421
457
|
end
|
|
422
458
|
end
|
|
423
459
|
|
|
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
|
|
465
|
+
|
|
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
|
|
481
|
+
|
|
424
482
|
# Configuration for generators.
|
|
425
483
|
#
|
|
426
484
|
# All options under this namespace default to `false` unless otherwise
|
|
@@ -512,6 +570,7 @@ module ViewComponent
|
|
|
512
570
|
# @param extensions [Array<String>] Extensions of which to return matching sidecar files.
|
|
513
571
|
def sidecar_files(extensions)
|
|
514
572
|
return [] unless identifier
|
|
573
|
+
return [] unless name
|
|
515
574
|
|
|
516
575
|
extensions = extensions.join(",")
|
|
517
576
|
|
|
@@ -573,7 +632,7 @@ module ViewComponent
|
|
|
573
632
|
# eager loading is disabled and the parent component is rendered before the child. In
|
|
574
633
|
# such a scenario, the parent will override ViewComponent::Base#render_template_for,
|
|
575
634
|
# meaning it will not be called for any children and thus not compile their templates.
|
|
576
|
-
if !child.
|
|
635
|
+
if !child.method_defined?(:render_template_for, false) && !child.__vc_compiled?
|
|
577
636
|
child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
578
637
|
def render_template_for(requested_details)
|
|
579
638
|
# Force compilation here so the compiler always redefines render_template_for.
|
|
@@ -596,7 +655,7 @@ module ViewComponent
|
|
|
596
655
|
# Set collection parameter to the extended component
|
|
597
656
|
child.with_collection_parameter(__vc_provided_collection_parameter)
|
|
598
657
|
|
|
599
|
-
if
|
|
658
|
+
if method_defined?(:render_template_for, false)
|
|
600
659
|
vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
|
|
601
660
|
|
|
602
661
|
vc_ancestor_calls.unshift(instance_method(:render_template_for))
|
|
@@ -611,6 +670,16 @@ module ViewComponent
|
|
|
611
670
|
__vc_compiler.compiled?
|
|
612
671
|
end
|
|
613
672
|
|
|
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
|
|
681
|
+
end
|
|
682
|
+
|
|
614
683
|
# @private
|
|
615
684
|
def __vc_ensure_compiled
|
|
616
685
|
__vc_compile unless __vc_compiled?
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "action_view/renderer/collection_renderer"
|
|
4
|
+
require "action_view/helpers/output_safety_helper"
|
|
4
5
|
|
|
5
6
|
module ViewComponent
|
|
6
7
|
class Collection
|
|
7
8
|
include Enumerable
|
|
9
|
+
include ActionView::Helpers::OutputSafetyHelper
|
|
10
|
+
|
|
8
11
|
attr_reader :component
|
|
9
12
|
|
|
10
13
|
delegate :size, to: :@collection
|
|
11
14
|
|
|
12
|
-
def render_in(view_context, &block)
|
|
13
|
-
components.map do |component|
|
|
15
|
+
def render_in(view_context, **_, &block)
|
|
16
|
+
rendered = components.map do |component|
|
|
14
17
|
component.render_in(view_context, &block)
|
|
15
|
-
end
|
|
18
|
+
end
|
|
19
|
+
safe_join(rendered, rendered_spacer(view_context))
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def each(&block)
|
|
19
23
|
components.each(&block)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
|
-
if Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
|
26
|
+
if defined?(Rails::VERSION) && Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
|
|
23
27
|
# Rails expects us to define `format` on all renderables,
|
|
24
28
|
# but we do not know the `format` of a ViewComponent until runtime.
|
|
25
29
|
def format
|
|
@@ -29,15 +33,15 @@ module ViewComponent
|
|
|
29
33
|
|
|
30
34
|
private
|
|
31
35
|
|
|
36
|
+
# Always rebuild child component instances per render to avoid leaking
|
|
37
|
+
# request-scoped state from a previous render into a later one (GHSA).
|
|
32
38
|
def components
|
|
33
|
-
return @components if defined? @components
|
|
34
|
-
|
|
35
39
|
iterator = ActionView::PartialIteration.new(@collection.size)
|
|
36
40
|
|
|
37
41
|
component.__vc_validate_collection_parameter!(validate_default: true)
|
|
38
42
|
|
|
39
|
-
@
|
|
40
|
-
component.new(**component_options(item, iterator)).tap do |
|
|
43
|
+
@collection.map do |item|
|
|
44
|
+
component.new(**component_options(item, iterator)).tap do |_|
|
|
41
45
|
iterator.iterate!
|
|
42
46
|
end
|
|
43
47
|
end
|
|
@@ -66,12 +70,12 @@ module ViewComponent
|
|
|
66
70
|
@options.merge(item_options)
|
|
67
71
|
end
|
|
68
72
|
|
|
73
|
+
# Render the spacer through a fresh `dup` so a collection rendered multiple
|
|
74
|
+
# times always gets a clean spacer instance.
|
|
69
75
|
def rendered_spacer(view_context)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
""
|
|
74
|
-
end
|
|
76
|
+
return "" unless @spacer_component
|
|
77
|
+
|
|
78
|
+
@spacer_component.dup.render_in(view_context)
|
|
75
79
|
end
|
|
76
80
|
end
|
|
77
81
|
end
|
|
@@ -52,12 +52,14 @@ module ViewComponent
|
|
|
52
52
|
@component.__vc_build_i18n_backend
|
|
53
53
|
|
|
54
54
|
CompileCache.register(@component)
|
|
55
|
+
|
|
56
|
+
@component.after_compile
|
|
55
57
|
end
|
|
56
58
|
end
|
|
57
59
|
|
|
58
|
-
# @
|
|
60
|
+
# @param requested_details [ActionView::TemplateDetails::Requested] i.e. locales, formats, variants
|
|
59
61
|
#
|
|
60
|
-
# @
|
|
62
|
+
# @return all matching compiled templates, in priority order based on the requested details from LookupContext
|
|
61
63
|
def find_templates_for(requested_details)
|
|
62
64
|
filtered_templates = @templates.select do |template|
|
|
63
65
|
template.details.matches?(requested_details)
|
|
@@ -113,11 +115,11 @@ module ViewComponent
|
|
|
113
115
|
.tally
|
|
114
116
|
.select { |_, count| count > 1 }
|
|
115
117
|
.each do |tally|
|
|
116
|
-
|
|
118
|
+
variant, this_format = tally.first
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
variant_string = " for variant `#{variant}`" if variant.present?
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
errors << "More than one #{this_format.upcase} template found#{variant_string} for #{@component}. "
|
|
121
123
|
end
|
|
122
124
|
|
|
123
125
|
default_template_types = @templates.each_with_object(Set.new) do |template, memo|
|
|
@@ -188,11 +190,11 @@ module ViewComponent
|
|
|
188
190
|
).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }
|
|
189
191
|
.uniq
|
|
190
192
|
.each do |method_name|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
templates << Template::InlineCall.new(
|
|
194
|
+
component: @component,
|
|
195
|
+
method_name: method_name,
|
|
196
|
+
defined_on_self: component_instance_methods_on_self.include?(method_name)
|
|
197
|
+
)
|
|
196
198
|
end
|
|
197
199
|
|
|
198
200
|
templates
|
|
@@ -19,6 +19,7 @@ module ViewComponent
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# @!attribute generate
|
|
22
|
+
#
|
|
22
23
|
# @return [ActiveSupport::OrderedOptions]
|
|
23
24
|
# The subset of configuration options relating to generators.
|
|
24
25
|
#
|
|
@@ -95,6 +96,7 @@ module ViewComponent
|
|
|
95
96
|
# in `spec/views/components/` rather than the default `spec/components/`.
|
|
96
97
|
|
|
97
98
|
# @!attribute previews
|
|
99
|
+
#
|
|
98
100
|
# @return [ActiveSupport::OrderedOptions]
|
|
99
101
|
# The subset of configuration options relating to previews.
|
|
100
102
|
#
|
|
@@ -124,6 +126,7 @@ module ViewComponent
|
|
|
124
126
|
#
|
|
125
127
|
|
|
126
128
|
# @!attribute instrumentation_enabled
|
|
129
|
+
#
|
|
127
130
|
# @return [Boolean]
|
|
128
131
|
# Whether ActiveSupport notifications are enabled.
|
|
129
132
|
# Defaults to `false`.
|
|
@@ -163,7 +166,7 @@ module ViewComponent
|
|
|
163
166
|
options = ActiveSupport::OrderedOptions.new
|
|
164
167
|
options.controller = "ViewComponentsController"
|
|
165
168
|
options.route = "/rails/view_components"
|
|
166
|
-
options.enabled = Rails.env.development? || Rails.env.test?
|
|
169
|
+
options.enabled = defined?(Rails.env) && (Rails.env.development? || Rails.env.test?)
|
|
167
170
|
options.default_layout = nil
|
|
168
171
|
options.paths = default_preview_paths
|
|
169
172
|
options
|
|
@@ -171,6 +174,7 @@ module ViewComponent
|
|
|
171
174
|
end
|
|
172
175
|
|
|
173
176
|
# @!attribute current
|
|
177
|
+
#
|
|
174
178
|
# @return [ViewComponent::Config]
|
|
175
179
|
# Returns the current ViewComponent::Config. This is persisted against this
|
|
176
180
|
# class so that config options remain accessible before the rest of
|
|
@@ -4,14 +4,30 @@ module ViewComponent
|
|
|
4
4
|
module Configurable
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
|
+
class_methods do
|
|
8
|
+
def config
|
|
9
|
+
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
|
|
10
|
+
superclass.config.inheritable_copy
|
|
11
|
+
else
|
|
12
|
+
ActiveSupport::OrderedOptions.new
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
yield config
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
included do
|
|
8
22
|
next if respond_to?(:config) && config.respond_to?(:view_component) && config.respond_to_missing?(:instrumentation_enabled)
|
|
9
23
|
|
|
10
|
-
include ActiveSupport::Configurable
|
|
11
|
-
|
|
12
24
|
configure do |config|
|
|
13
25
|
config.view_component ||= ActiveSupport::InheritableOptions.new
|
|
14
26
|
end
|
|
27
|
+
|
|
28
|
+
def config
|
|
29
|
+
self.class.config
|
|
30
|
+
end
|
|
15
31
|
end
|
|
16
32
|
end
|
|
17
33
|
end
|
|
@@ -50,11 +50,13 @@ module ViewComponent
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
config.
|
|
53
|
+
config.after_routes_loaded do
|
|
54
54
|
ActiveSupport.on_load(:view_component) do
|
|
55
55
|
if defined?(Sprockets::Rails)
|
|
56
56
|
include Sprockets::Rails::Helper
|
|
57
57
|
|
|
58
|
+
app = Rails.application
|
|
59
|
+
|
|
58
60
|
# Copy relevant config to VC context
|
|
59
61
|
# See: https://github.com/rails/sprockets-rails/blob/266ec49f3c7c96018dd75f9dc4f9b62fe3f7eecf/lib/sprockets/railtie.rb#L245
|
|
60
62
|
self.debug_assets = app.config.assets.debug
|
|
@@ -77,7 +79,15 @@ module ViewComponent
|
|
|
77
79
|
|
|
78
80
|
initializer "view_component.eager_load_actions" do
|
|
79
81
|
ActiveSupport.on_load(:after_initialize) do
|
|
80
|
-
|
|
82
|
+
if Rails.application.config.eager_load
|
|
83
|
+
ActiveSupport::Notifications.instrument("compile.view_component") do
|
|
84
|
+
ViewComponent::Base.descendants.each do |component|
|
|
85
|
+
next if component.anonymous?
|
|
86
|
+
|
|
87
|
+
component.__vc_compile
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
81
91
|
end
|
|
82
92
|
end
|
|
83
93
|
|
|
@@ -8,18 +8,31 @@ module ViewComponent # :nodoc:
|
|
|
8
8
|
mod.prepend(self) unless self <= ViewComponent::Instrumentation
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def render_in(view_context, &block)
|
|
11
|
+
def render_in(view_context, **_, &block)
|
|
12
12
|
return super if !Rails.application.config.view_component.instrumentation_enabled.present?
|
|
13
13
|
|
|
14
|
+
payload = {
|
|
15
|
+
name: self.class.name,
|
|
16
|
+
identifier: self.class.identifier,
|
|
17
|
+
view_identifier: nil
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
ActiveSupport::Notifications.instrument(
|
|
15
21
|
"render.view_component",
|
|
16
|
-
|
|
17
|
-
name: self.class.name,
|
|
18
|
-
identifier: self.class.identifier
|
|
19
|
-
}
|
|
22
|
+
payload
|
|
20
23
|
) do
|
|
21
|
-
super
|
|
24
|
+
result = super
|
|
25
|
+
payload[:view_identifier] = @__vc_instrumentation_view_identifier
|
|
26
|
+
result
|
|
22
27
|
end
|
|
28
|
+
ensure
|
|
29
|
+
@__vc_instrumentation_view_identifier = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def around_render
|
|
33
|
+
result = super
|
|
34
|
+
@__vc_instrumentation_view_identifier = current_template&.path
|
|
35
|
+
result
|
|
23
36
|
end
|
|
24
37
|
end
|
|
25
38
|
end
|
|
@@ -40,6 +40,8 @@ module ViewComponent # :nodoc:
|
|
|
40
40
|
|
|
41
41
|
# Returns the arguments for rendering of the component in its layout
|
|
42
42
|
def render_args(example, params: {})
|
|
43
|
+
raise AbstractController::ActionNotFound, "#{example} is not a valid preview example" unless examples.include?(example.to_s)
|
|
44
|
+
|
|
43
45
|
example_params_names = instance_method(example).parameters.map(&:last)
|
|
44
46
|
provided_params = params.slice(*example_params_names).to_h.symbolize_keys
|
|
45
47
|
result = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)
|
|
@@ -95,7 +97,7 @@ module ViewComponent # :nodoc:
|
|
|
95
97
|
# @private
|
|
96
98
|
def __vc_load_previews
|
|
97
99
|
Array(preview_paths).each do |preview_path|
|
|
98
|
-
Dir["#{preview_path}/**/*preview.rb"].sort.each { |file|
|
|
100
|
+
Dir["#{preview_path}/**/*preview.rb"].sort.each { |file| require file }
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
|
data/lib/view_component/slot.rb
CHANGED
|
@@ -6,7 +6,7 @@ module ViewComponent
|
|
|
6
6
|
class Slot
|
|
7
7
|
include ViewComponent::WithContentHelper
|
|
8
8
|
|
|
9
|
-
attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content
|
|
9
|
+
attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content, :__vc_content_block_virtual_path
|
|
10
10
|
|
|
11
11
|
def initialize(parent)
|
|
12
12
|
@parent = parent
|
|
@@ -58,7 +58,7 @@ module ViewComponent
|
|
|
58
58
|
if defined?(@__vc_content_block)
|
|
59
59
|
# render_in is faster than `parent.render`
|
|
60
60
|
@__vc_component_instance.render_in(view_context) do |*args|
|
|
61
|
-
@parent.
|
|
61
|
+
@parent.with_captured_virtual_path(@__vc_content_block_virtual_path) do
|
|
62
62
|
@__vc_content_block.call(*args)
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -68,7 +68,7 @@ module ViewComponent
|
|
|
68
68
|
elsif defined?(@__vc_content)
|
|
69
69
|
@__vc_content
|
|
70
70
|
elsif defined?(@__vc_content_block)
|
|
71
|
-
@parent.
|
|
71
|
+
@parent.with_captured_virtual_path(@__vc_content_block_virtual_path) do
|
|
72
72
|
view_context.capture(&@__vc_content_block)
|
|
73
73
|
end
|
|
74
74
|
elsif defined?(@__vc_content_set_by_with_content)
|
|
@@ -96,7 +96,6 @@ module ViewComponent
|
|
|
96
96
|
# end
|
|
97
97
|
# end
|
|
98
98
|
# end
|
|
99
|
-
#
|
|
100
99
|
def method_missing(symbol, *args, **kwargs, &block)
|
|
101
100
|
@__vc_component_instance.public_send(symbol, *args, **kwargs, &block)
|
|
102
101
|
end
|
|
@@ -259,7 +259,7 @@ module ViewComponent
|
|
|
259
259
|
|
|
260
260
|
setter_method_name = :"with_#{poly_slot_name}"
|
|
261
261
|
|
|
262
|
-
if
|
|
262
|
+
if method_defined?(setter_method_name)
|
|
263
263
|
raise AlreadyDefinedPolymorphicSlotSetterError.new(setter_method_name, poly_slot_name)
|
|
264
264
|
end
|
|
265
265
|
|
|
@@ -380,6 +380,7 @@ module ViewComponent
|
|
|
380
380
|
def __vc_set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
|
|
381
381
|
slot_definition ||= self.class.registered_slots[slot_name]
|
|
382
382
|
slot = Slot.new(self)
|
|
383
|
+
captured_block_virtual_path = nil
|
|
383
384
|
|
|
384
385
|
# Passing the block to the sub-component wrapper like this has two
|
|
385
386
|
# benefits:
|
|
@@ -390,7 +391,13 @@ module ViewComponent
|
|
|
390
391
|
# 2. Since we have to pass block content to components when calling
|
|
391
392
|
# `render`, evaluating the block here would require us to call
|
|
392
393
|
# `view_context.capture` twice, which is slower
|
|
393
|
-
|
|
394
|
+
if block
|
|
395
|
+
slot.__vc_content_block = block
|
|
396
|
+
# Capture the virtual path at the time the block is defined, so that
|
|
397
|
+
# translations resolve relative to where the block was created, not where it's rendered
|
|
398
|
+
captured_block_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
|
399
|
+
slot.__vc_content_block_virtual_path = captured_block_virtual_path
|
|
400
|
+
end
|
|
394
401
|
|
|
395
402
|
# If class
|
|
396
403
|
if slot_definition[:renderable]
|
|
@@ -408,7 +415,9 @@ module ViewComponent
|
|
|
408
415
|
renderable_value =
|
|
409
416
|
if block
|
|
410
417
|
renderable_function.call(*args, **kwargs) do |*rargs|
|
|
411
|
-
|
|
418
|
+
with_captured_virtual_path(captured_block_virtual_path) do
|
|
419
|
+
view_context.capture(*rargs, &block)
|
|
420
|
+
end
|
|
412
421
|
end
|
|
413
422
|
else
|
|
414
423
|
renderable_function.call(*args, **kwargs)
|
|
@@ -7,6 +7,7 @@ module ViewComponent
|
|
|
7
7
|
# Returns a block that can be used to visit the path of the inline rendered component.
|
|
8
8
|
# @param fragment [Nokogiri::Fragment] The fragment returned from `render_inline`.
|
|
9
9
|
# @param layout [String] The (optional) layout to use.
|
|
10
|
+
#
|
|
10
11
|
# @return [Proc] A block that can be used to visit the path of the inline rendered component.
|
|
11
12
|
def with_rendered_component_path(fragment, layout: false, &block)
|
|
12
13
|
file = Tempfile.new(
|