view_component 2.52.0 → 2.62.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.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/controllers/concerns/view_component/preview_actions.rb +101 -0
- data/app/controllers/view_components_controller.rb +1 -87
- data/app/helpers/preview_helper.rb +3 -3
- data/docs/CHANGELOG.md +285 -1
- data/lib/rails/generators/abstract_generator.rb +4 -4
- data/lib/view_component/base.rb +95 -33
- data/lib/view_component/collection.rb +9 -2
- data/lib/view_component/compile_cache.rb +1 -2
- data/lib/view_component/compiler.rb +31 -18
- data/lib/view_component/content_areas.rb +1 -1
- data/lib/view_component/docs_builder_component.rb +1 -1
- data/lib/view_component/engine.rb +6 -21
- data/lib/view_component/polymorphic_slots.rb +22 -1
- data/lib/view_component/preview.rb +12 -9
- data/lib/view_component/render_component_to_string_helper.rb +1 -1
- data/lib/view_component/render_preview_helper.rb +50 -0
- 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/slotable.rb +5 -6
- data/lib/view_component/slotable_v2.rb +35 -17
- data/lib/view_component/test_helpers.rb +39 -7
- data/lib/view_component/translatable.rb +13 -14
- data/lib/view_component/version.rb +1 -1
- data/lib/view_component.rb +0 -2
- metadata +7 -7
- data/lib/view_component/global_output_buffer.rb +0 -99
- data/lib/view_component/output_buffer_stack.rb +0 -67
data/lib/view_component/base.rb
CHANGED
@@ -9,14 +9,17 @@ require "view_component/polymorphic_slots"
|
|
9
9
|
require "view_component/previewable"
|
10
10
|
require "view_component/slotable"
|
11
11
|
require "view_component/slotable_v2"
|
12
|
+
require "view_component/translatable"
|
12
13
|
require "view_component/with_content_helper"
|
13
14
|
|
14
15
|
module ViewComponent
|
15
16
|
class Base < ActionView::Base
|
16
17
|
include ActiveSupport::Configurable
|
17
18
|
include ViewComponent::ContentAreas
|
19
|
+
include ViewComponent::PolymorphicSlots
|
18
20
|
include ViewComponent::Previewable
|
19
21
|
include ViewComponent::SlotableV2
|
22
|
+
include ViewComponent::Translatable
|
20
23
|
include ViewComponent::WithContentHelper
|
21
24
|
|
22
25
|
ViewContextCalledBeforeRenderError = Class.new(StandardError)
|
@@ -29,8 +32,25 @@ module ViewComponent
|
|
29
32
|
class_attribute :content_areas
|
30
33
|
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
|
31
34
|
|
35
|
+
# Config option that strips trailing whitespace in templates before compiling them.
|
36
|
+
class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false
|
37
|
+
self.__vc_strip_trailing_whitespace = false # class_attribute:default doesn't work until Rails 5.2
|
38
|
+
|
32
39
|
attr_accessor :__vc_original_view_context
|
33
40
|
|
41
|
+
# Components render in their own view context. Helpers and other functionality
|
42
|
+
# require a reference to the original Rails view context, an instance of
|
43
|
+
# `ActionView::Base`. Use this method to set a reference to the original
|
44
|
+
# view context. Objects that implement this method will render in the component's
|
45
|
+
# view context, while objects that don't will render in the original view context
|
46
|
+
# so helpers, etc work as expected.
|
47
|
+
#
|
48
|
+
# @param view_context [ActionView::Base] The original view context.
|
49
|
+
# @return [void]
|
50
|
+
def set_original_view_context(view_context)
|
51
|
+
self.__vc_original_view_context = view_context
|
52
|
+
end
|
53
|
+
|
34
54
|
# EXPERIMENTAL: This API is experimental and may be removed at any time.
|
35
55
|
# Hook for allowing components to do work as part of the compilation process.
|
36
56
|
#
|
@@ -66,10 +86,12 @@ module ViewComponent
|
|
66
86
|
#
|
67
87
|
# @return [String]
|
68
88
|
def render_in(view_context, &block)
|
89
|
+
self.class.compile(raise_errors: true)
|
90
|
+
|
69
91
|
@view_context = view_context
|
70
92
|
self.__vc_original_view_context ||= view_context
|
71
93
|
|
72
|
-
@output_buffer = ActionView::OutputBuffer.new
|
94
|
+
@output_buffer = ActionView::OutputBuffer.new
|
73
95
|
|
74
96
|
@lookup_context ||= view_context.lookup_context
|
75
97
|
|
@@ -104,7 +126,7 @@ module ViewComponent
|
|
104
126
|
before_render
|
105
127
|
|
106
128
|
if render?
|
107
|
-
|
129
|
+
render_template_for(@__vc_variant).to_s + _output_postamble
|
108
130
|
else
|
109
131
|
""
|
110
132
|
end
|
@@ -112,19 +134,20 @@ module ViewComponent
|
|
112
134
|
@current_template = old_current_template
|
113
135
|
end
|
114
136
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
137
|
+
# Subclass components that call `super` inside their template code will cause a
|
138
|
+
# double render if they emit the result:
|
139
|
+
#
|
140
|
+
# ```erb
|
141
|
+
# <%= super %> # double-renders
|
142
|
+
# <% super %> # does not double-render
|
143
|
+
# ```
|
144
|
+
#
|
145
|
+
# Calls `super`, returning `nil` to avoid rendering the result twice.
|
146
|
+
def render_parent
|
147
|
+
mtd = @__vc_variant ? "call_#{@__vc_variant}" : "call"
|
148
|
+
method(mtd).super_method.call
|
149
|
+
nil
|
126
150
|
end
|
127
|
-
# :nocov:
|
128
151
|
|
129
152
|
# EXPERIMENTAL: Optional content to be returned after the rendered template.
|
130
153
|
#
|
@@ -157,7 +180,8 @@ module ViewComponent
|
|
157
180
|
end
|
158
181
|
|
159
182
|
# @private
|
160
|
-
def initialize(*)
|
183
|
+
def initialize(*)
|
184
|
+
end
|
161
185
|
|
162
186
|
# Re-use original view_context if we're not rendering a component.
|
163
187
|
#
|
@@ -167,8 +191,8 @@ module ViewComponent
|
|
167
191
|
#
|
168
192
|
# @private
|
169
193
|
def render(options = {}, args = {}, &block)
|
170
|
-
if options.
|
171
|
-
options.__vc_original_view_context
|
194
|
+
if options.respond_to?(:set_original_view_context)
|
195
|
+
options.set_original_view_context(self.__vc_original_view_context)
|
172
196
|
super
|
173
197
|
else
|
174
198
|
__vc_original_view_context.render(options, args, &block)
|
@@ -285,7 +309,9 @@ module ViewComponent
|
|
285
309
|
|
286
310
|
# Set the controller used for testing components:
|
287
311
|
#
|
288
|
-
#
|
312
|
+
# ```ruby
|
313
|
+
# config.view_component.test_controller = "MyTestController"
|
314
|
+
# ```
|
289
315
|
#
|
290
316
|
# Defaults to ApplicationController. Can also be configured on a per-test
|
291
317
|
# basis using `with_controller_class`.
|
@@ -295,13 +321,17 @@ module ViewComponent
|
|
295
321
|
|
296
322
|
# Set if render monkey patches should be included or not in Rails <6.1:
|
297
323
|
#
|
298
|
-
#
|
324
|
+
# ```ruby
|
325
|
+
# config.view_component.render_monkey_patch_enabled = false
|
326
|
+
# ```
|
299
327
|
#
|
300
328
|
mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
|
301
329
|
|
302
330
|
# Path for component files
|
303
331
|
#
|
304
|
-
#
|
332
|
+
# ```ruby
|
333
|
+
# config.view_component.view_component_path = "app/my_components"
|
334
|
+
# ```
|
305
335
|
#
|
306
336
|
# Defaults to `app/components`.
|
307
337
|
#
|
@@ -309,7 +339,9 @@ module ViewComponent
|
|
309
339
|
|
310
340
|
# Parent class for generated components
|
311
341
|
#
|
312
|
-
#
|
342
|
+
# ```ruby
|
343
|
+
# config.view_component.component_parent_class = "MyBaseComponent"
|
344
|
+
# ```
|
313
345
|
#
|
314
346
|
# Defaults to nil. If this is falsy, generators will use
|
315
347
|
# "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
|
@@ -325,25 +357,33 @@ module ViewComponent
|
|
325
357
|
#
|
326
358
|
# Always generate a component with a sidecar directory:
|
327
359
|
#
|
328
|
-
#
|
360
|
+
# ```ruby
|
361
|
+
# config.view_component.generate.sidecar = true
|
362
|
+
# ```
|
329
363
|
#
|
330
364
|
# #### #stimulus_controller
|
331
365
|
#
|
332
366
|
# Always generate a Stimulus controller alongside the component:
|
333
367
|
#
|
334
|
-
#
|
368
|
+
# ```ruby
|
369
|
+
# config.view_component.generate.stimulus_controller = true
|
370
|
+
# ```
|
335
371
|
#
|
336
372
|
# #### #locale
|
337
373
|
#
|
338
374
|
# Always generate translations file alongside the component:
|
339
375
|
#
|
340
|
-
#
|
376
|
+
# ```ruby
|
377
|
+
# config.view_component.generate.locale = true
|
378
|
+
# ```
|
341
379
|
#
|
342
380
|
# #### #distinct_locale_files
|
343
381
|
#
|
344
382
|
# Always generate as many translations files as available locales:
|
345
383
|
#
|
346
|
-
#
|
384
|
+
# ```ruby
|
385
|
+
# config.view_component.generate.distinct_locale_files = true
|
386
|
+
# ```
|
347
387
|
#
|
348
388
|
# One file will be generated for each configured `I18n.available_locales`,
|
349
389
|
# falling back to `[:en]` when no `available_locales` is defined.
|
@@ -352,7 +392,9 @@ module ViewComponent
|
|
352
392
|
#
|
353
393
|
# Always generate preview alongside the component:
|
354
394
|
#
|
355
|
-
#
|
395
|
+
# ```ruby
|
396
|
+
# config.view_component.generate.preview = true
|
397
|
+
# ```
|
356
398
|
#
|
357
399
|
# Defaults to `false`.
|
358
400
|
mattr_accessor :generate, instance_writer: false, default: ActiveSupport::OrderedOptions.new(false)
|
@@ -406,7 +448,9 @@ module ViewComponent
|
|
406
448
|
|
407
449
|
# Render a component for each element in a collection ([documentation](/guide/collections)):
|
408
450
|
#
|
409
|
-
#
|
451
|
+
# ```ruby
|
452
|
+
# render(ProductsComponent.with_collection(@products, foo: :bar))
|
453
|
+
# ```
|
410
454
|
#
|
411
455
|
# @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
|
412
456
|
# @param args [Arguments] Arguments to pass to the ViewComponent every time.
|
@@ -502,13 +546,35 @@ module ViewComponent
|
|
502
546
|
|
503
547
|
# Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
|
504
548
|
#
|
505
|
-
#
|
549
|
+
# ```ruby
|
550
|
+
# with_collection_parameter :item
|
551
|
+
# ```
|
506
552
|
#
|
507
553
|
# @param parameter [Symbol] The parameter name used when rendering elements of a collection.
|
508
554
|
def with_collection_parameter(parameter)
|
509
555
|
@provided_collection_parameter = parameter
|
510
556
|
end
|
511
557
|
|
558
|
+
# Strips trailing whitespace from templates before compiling them.
|
559
|
+
#
|
560
|
+
# ```ruby
|
561
|
+
# class MyComponent < ViewComponent::Base
|
562
|
+
# strip_trailing_whitespace
|
563
|
+
# end
|
564
|
+
# ```
|
565
|
+
#
|
566
|
+
# @param value [Boolean] Whether or not to strip newlines.
|
567
|
+
def strip_trailing_whitespace(value = true)
|
568
|
+
self.__vc_strip_trailing_whitespace = value
|
569
|
+
end
|
570
|
+
|
571
|
+
# Whether trailing whitespace will be stripped before compilation.
|
572
|
+
#
|
573
|
+
# @return [Boolean]
|
574
|
+
def strip_trailing_whitespace?
|
575
|
+
__vc_strip_trailing_whitespace
|
576
|
+
end
|
577
|
+
|
512
578
|
# Ensure the component initializer accepts the
|
513
579
|
# collection parameter. By default, we don't
|
514
580
|
# validate that the default parameter name
|
@@ -556,11 +622,7 @@ module ViewComponent
|
|
556
622
|
|
557
623
|
# @private
|
558
624
|
def collection_parameter
|
559
|
-
|
560
|
-
provided_collection_parameter
|
561
|
-
else
|
562
|
-
name && name.demodulize.underscore.chomp("_component").to_sym
|
563
|
-
end
|
625
|
+
provided_collection_parameter || name && name.demodulize.underscore.chomp("_component").to_sym
|
564
626
|
end
|
565
627
|
|
566
628
|
# @private
|
@@ -10,10 +10,17 @@ module ViewComponent
|
|
10
10
|
delegate :format, to: :component
|
11
11
|
delegate :size, to: :@collection
|
12
12
|
|
13
|
+
attr_accessor :__vc_original_view_context
|
14
|
+
|
15
|
+
def set_original_view_context(view_context)
|
16
|
+
self.__vc_original_view_context = view_context
|
17
|
+
end
|
18
|
+
|
13
19
|
def render_in(view_context, &block)
|
14
20
|
components.map do |component|
|
21
|
+
component.set_original_view_context(__vc_original_view_context)
|
15
22
|
component.render_in(view_context, &block)
|
16
|
-
end.join.html_safe
|
23
|
+
end.join.html_safe
|
17
24
|
end
|
18
25
|
|
19
26
|
def components
|
@@ -54,7 +61,7 @@ module ViewComponent
|
|
54
61
|
end
|
55
62
|
|
56
63
|
def component_options(item, iterator)
|
57
|
-
item_options = {
|
64
|
+
item_options = {component.collection_parameter => item}
|
58
65
|
item_options[component.collection_counter_parameter] = iterator.index + 1 if component.counter_argument_present?
|
59
66
|
item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present?
|
60
67
|
|
@@ -31,7 +31,11 @@ module ViewComponent
|
|
31
31
|
return if compiled? && !force
|
32
32
|
return if component_class == ViewComponent::Base
|
33
33
|
|
34
|
+
component_class.superclass.compile(raise_errors: raise_errors) if should_compile_superclass?
|
35
|
+
|
34
36
|
with_lock do
|
37
|
+
CompileCache.invalidate_class!(component_class)
|
38
|
+
|
35
39
|
subclass_instance_methods = component_class.instance_methods(false)
|
36
40
|
|
37
41
|
if subclass_instance_methods.include?(:with_content) && raise_errors
|
@@ -64,19 +68,22 @@ module ViewComponent
|
|
64
68
|
# as Ruby warns when redefining a method.
|
65
69
|
method_name = call_method_name(template[:variant])
|
66
70
|
|
67
|
-
if component_class.instance_methods
|
68
|
-
component_class.send(:
|
71
|
+
if component_class.instance_methods.include?(method_name.to_sym)
|
72
|
+
component_class.send(:undef_method, method_name.to_sym)
|
69
73
|
end
|
70
74
|
|
75
|
+
# rubocop:disable Style/EvalWithLocation
|
71
76
|
component_class.class_eval <<-RUBY, template[:path], 0
|
72
77
|
def #{method_name}
|
73
78
|
#{compiled_template(template[:path])}
|
74
79
|
end
|
75
80
|
RUBY
|
81
|
+
# rubocop:enable Style/EvalWithLocation
|
76
82
|
end
|
77
83
|
|
78
84
|
define_render_template_for
|
79
85
|
|
86
|
+
component_class.build_i18n_backend
|
80
87
|
component_class._after_compile
|
81
88
|
|
82
89
|
CompileCache.register(component_class)
|
@@ -91,18 +98,14 @@ module ViewComponent
|
|
91
98
|
end
|
92
99
|
end
|
93
100
|
|
94
|
-
def reset_render_template_for
|
95
|
-
if component_class.instance_methods(false).include?(:render_template_for)
|
96
|
-
component_class.send(:remove_method, :render_template_for)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
101
|
private
|
101
102
|
|
102
103
|
attr_reader :component_class
|
103
104
|
|
104
105
|
def define_render_template_for
|
105
|
-
|
106
|
+
if component_class.instance_methods.include?(:render_template_for)
|
107
|
+
component_class.send(:undef_method, :render_template_for)
|
108
|
+
end
|
106
109
|
|
107
110
|
variant_elsifs = variants.compact.uniq.map do |variant|
|
108
111
|
"elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
|
@@ -150,15 +153,15 @@ module ViewComponent
|
|
150
153
|
end
|
151
154
|
|
152
155
|
invalid_variants =
|
153
|
-
templates
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
156
|
+
templates
|
157
|
+
.group_by { |template| template[:variant] }
|
158
|
+
.map { |variant, grouped| variant if grouped.length > 1 }
|
159
|
+
.compact
|
160
|
+
.sort
|
158
161
|
|
159
162
|
unless invalid_variants.empty?
|
160
163
|
errors <<
|
161
|
-
"More than one template found for #{
|
164
|
+
"More than one template found for #{"variant".pluralize(invalid_variants.count)} " \
|
162
165
|
"#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
|
163
166
|
"There can only be one template file per variant."
|
164
167
|
end
|
@@ -176,8 +179,8 @@ module ViewComponent
|
|
176
179
|
count = duplicate_template_file_and_inline_variant_calls.count
|
177
180
|
|
178
181
|
errors <<
|
179
|
-
"Template #{
|
180
|
-
"found for #{
|
182
|
+
"Template #{"file".pluralize(count)} and inline render #{"method".pluralize(count)} " \
|
183
|
+
"found for #{"variant".pluralize(count)} " \
|
181
184
|
"#{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} " \
|
182
185
|
"in #{component_class}. " \
|
183
186
|
"There can only be a template file or inline render method per variant."
|
@@ -235,8 +238,9 @@ module ViewComponent
|
|
235
238
|
end
|
236
239
|
|
237
240
|
def compiled_template(file_path)
|
238
|
-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).
|
241
|
+
handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
|
239
242
|
template = File.read(file_path)
|
243
|
+
template.rstrip! if component_class.strip_trailing_whitespace?
|
240
244
|
|
241
245
|
if handler.method(:call).parameters.length > 1
|
242
246
|
handler.call(component_class, template)
|
@@ -258,5 +262,14 @@ module ViewComponent
|
|
258
262
|
"call"
|
259
263
|
end
|
260
264
|
end
|
265
|
+
|
266
|
+
def should_compile_superclass?
|
267
|
+
development? &&
|
268
|
+
templates.empty? &&
|
269
|
+
!(
|
270
|
+
component_class.instance_methods(false).include?(:call) ||
|
271
|
+
component_class.private_instance_methods(false).include?(:call)
|
272
|
+
)
|
273
|
+
end
|
261
274
|
end
|
262
275
|
end
|
@@ -28,7 +28,7 @@ module ViewComponent
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def types
|
31
|
-
" → [#{@method.tag(:return).types.join(
|
31
|
+
" → [#{@method.tag(:return).types.join(",")}]" if @method.tag(:return)&.types && show_types?
|
32
32
|
end
|
33
33
|
|
34
34
|
def signature_or_name
|
@@ -20,7 +20,6 @@ module ViewComponent
|
|
20
20
|
options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
|
21
21
|
options.preview_route ||= ViewComponent::Base.preview_route
|
22
22
|
options.preview_controller ||= ViewComponent::Base.preview_controller
|
23
|
-
options.use_global_output_buffer = false if options.use_global_output_buffer.nil?
|
24
23
|
|
25
24
|
if options.show_previews
|
26
25
|
options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?(
|
@@ -58,26 +57,12 @@ module ViewComponent
|
|
58
57
|
end
|
59
58
|
end
|
60
59
|
|
61
|
-
initializer "view_component.enable_global_output_buffer" do |app|
|
62
|
-
ActiveSupport.on_load(:view_component) do
|
63
|
-
env_use_gob = ENV.fetch("VIEW_COMPONENT_USE_GLOBAL_OUTPUT_BUFFER", "false") == "true"
|
64
|
-
config_use_gob = app.config.view_component.use_global_output_buffer
|
65
|
-
|
66
|
-
if config_use_gob || env_use_gob
|
67
|
-
# :nocov:
|
68
|
-
app.config.view_component.use_global_output_buffer = true
|
69
|
-
ViewComponent::Base.prepend(ViewComponent::GlobalOutputBuffer)
|
70
|
-
ActionView::Base.prepend(ViewComponent::GlobalOutputBuffer::ActionViewMods)
|
71
|
-
# :nocov:
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
60
|
initializer "view_component.set_autoload_paths" do |app|
|
77
61
|
options = app.config.view_component
|
78
62
|
|
79
63
|
if options.show_previews && !options.preview_paths.empty?
|
80
|
-
ActiveSupport::Dependencies.autoload_paths
|
64
|
+
paths_to_add = options.preview_paths - ActiveSupport::Dependencies.autoload_paths
|
65
|
+
ActiveSupport::Dependencies.autoload_paths.concat(paths_to_add) if paths_to_add.any?
|
81
66
|
end
|
82
67
|
end
|
83
68
|
|
@@ -133,10 +118,10 @@ module ViewComponent
|
|
133
118
|
|
134
119
|
initializer "compiler mode" do |app|
|
135
120
|
ViewComponent::Compiler.mode = if Rails.env.development? || Rails.env.test?
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
121
|
+
ViewComponent::Compiler::DEVELOPMENT_MODE
|
122
|
+
else
|
123
|
+
ViewComponent::Compiler::PRODUCTION_MODE
|
124
|
+
end
|
140
125
|
end
|
141
126
|
|
142
127
|
config.after_initialize do |app|
|
@@ -5,6 +5,17 @@ module ViewComponent
|
|
5
5
|
# In older rails versions, using a concern isn't a good idea here because they appear to not work with
|
6
6
|
# Module#prepend and class methods.
|
7
7
|
def self.included(base)
|
8
|
+
if base != ViewComponent::Base
|
9
|
+
# :nocov:
|
10
|
+
location = Kernel.caller_locations(1, 1)[0]
|
11
|
+
|
12
|
+
warn(
|
13
|
+
"warning: ViewComponent::PolymorphicSlots is now included in ViewComponent::Base by default " \
|
14
|
+
"and can be removed from #{location.path}:#{location.lineno}"
|
15
|
+
)
|
16
|
+
# :nocov:
|
17
|
+
end
|
18
|
+
|
8
19
|
base.singleton_class.prepend(ClassMethods)
|
9
20
|
base.include(InstanceMethods)
|
10
21
|
end
|
@@ -46,12 +57,22 @@ module ViewComponent
|
|
46
57
|
end
|
47
58
|
|
48
59
|
define_method(setter_name) do |*args, &block|
|
60
|
+
ViewComponent::Deprecation.warn(
|
61
|
+
"polymorphic slot setters like `#{setter_name}` are deprecated and will be removed in " \
|
62
|
+
"ViewComponent v3.0.0.\n\nUse `with_#{setter_name}` instead."
|
63
|
+
)
|
64
|
+
|
49
65
|
set_polymorphic_slot(slot_name, poly_type, *args, &block)
|
50
66
|
end
|
51
67
|
ruby2_keywords(setter_name.to_sym) if respond_to?(:ruby2_keywords, true)
|
68
|
+
|
69
|
+
define_method("with_#{setter_name}") do |*args, &block|
|
70
|
+
set_polymorphic_slot(slot_name, poly_type, *args, &block)
|
71
|
+
end
|
72
|
+
ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
|
52
73
|
end
|
53
74
|
|
54
|
-
|
75
|
+
registered_slots[slot_name] = {
|
55
76
|
collection: collection,
|
56
77
|
renderable_hash: renderable_hash
|
57
78
|
}
|
@@ -14,7 +14,7 @@ module ViewComponent # :nodoc:
|
|
14
14
|
block: block,
|
15
15
|
component: component,
|
16
16
|
locals: {},
|
17
|
-
template: "view_components/preview"
|
17
|
+
template: "view_components/preview"
|
18
18
|
}
|
19
19
|
end
|
20
20
|
|
@@ -30,7 +30,8 @@ module ViewComponent # :nodoc:
|
|
30
30
|
class << self
|
31
31
|
# Returns all component preview classes.
|
32
32
|
def all
|
33
|
-
load_previews
|
33
|
+
load_previews
|
34
|
+
|
34
35
|
descendants
|
35
36
|
end
|
36
37
|
|
@@ -65,10 +66,12 @@ module ViewComponent # :nodoc:
|
|
65
66
|
name.chomp("Preview").underscore
|
66
67
|
end
|
67
68
|
|
69
|
+
# rubocop:disable Style/TrivialAccessors
|
68
70
|
# Setter for layout name.
|
69
71
|
def layout(layout_name)
|
70
72
|
@layout = layout_name
|
71
73
|
end
|
74
|
+
# rubocop:enable Style/TrivialAccessors
|
72
75
|
|
73
76
|
# Returns the relative path (from preview_path) to the preview example template if the template exists
|
74
77
|
def preview_example_template_path(example)
|
@@ -86,26 +89,26 @@ module ViewComponent # :nodoc:
|
|
86
89
|
end
|
87
90
|
|
88
91
|
path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
|
89
|
-
Pathname.new(path)
|
90
|
-
relative_path_from(Pathname.new(preview_path))
|
91
|
-
to_s
|
92
|
-
sub(/\..*$/, "")
|
92
|
+
Pathname.new(path)
|
93
|
+
.relative_path_from(Pathname.new(preview_path))
|
94
|
+
.to_s
|
95
|
+
.sub(/\..*$/, "")
|
93
96
|
end
|
94
97
|
|
95
98
|
# Returns the method body for the example from the preview file.
|
96
99
|
def preview_source(example)
|
97
|
-
source =
|
100
|
+
source = instance_method(example.to_sym).source.split("\n")
|
98
101
|
source[1...(source.size - 1)].join("\n")
|
99
102
|
end
|
100
103
|
|
101
|
-
private
|
102
|
-
|
103
104
|
def load_previews
|
104
105
|
Array(preview_paths).each do |preview_path|
|
105
106
|
Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
|
106
107
|
end
|
107
108
|
end
|
108
109
|
|
110
|
+
private
|
111
|
+
|
109
112
|
def preview_paths
|
110
113
|
Base.preview_paths
|
111
114
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
module RenderPreviewHelper
|
5
|
+
# Render a preview inline. Internally sets `page` to be a `Capybara::Node::Simple`,
|
6
|
+
# allowing for Capybara assertions to be used:
|
7
|
+
#
|
8
|
+
# ```ruby
|
9
|
+
# render_preview(:default)
|
10
|
+
# assert_text("Hello, World!")
|
11
|
+
# ```
|
12
|
+
#
|
13
|
+
# Note: `#rendered_preview` expects a preview to be defined with the same class
|
14
|
+
# name as the calling test, but with `Test` replaced with `Preview`:
|
15
|
+
#
|
16
|
+
# MyComponentTest -> MyComponentPreview etc.
|
17
|
+
#
|
18
|
+
# In RSpec, `Preview` is appended to `described_class`.
|
19
|
+
#
|
20
|
+
# @param preview [String] The name of the preview to be rendered.
|
21
|
+
# @return [Nokogiri::HTML]
|
22
|
+
def render_preview(name)
|
23
|
+
begin
|
24
|
+
preview_klass = if respond_to?(:described_class)
|
25
|
+
if described_class.nil?
|
26
|
+
raise "`render_preview` expected a described_class, but it is nil."
|
27
|
+
end
|
28
|
+
|
29
|
+
"#{described_class}Preview"
|
30
|
+
else
|
31
|
+
self.class.name.gsub("Test", "Preview")
|
32
|
+
end
|
33
|
+
preview_klass = preview_klass.constantize
|
34
|
+
rescue NameError
|
35
|
+
raise NameError.new(
|
36
|
+
"`render_preview` expected to find #{preview_klass}, but it does not exist."
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
previews_controller = build_controller(ViewComponent::Base.preview_controller.constantize)
|
41
|
+
previews_controller.request.params[:path] = "#{preview_klass.preview_name}/#{name}"
|
42
|
+
previews_controller.response = ActionDispatch::Response.new
|
43
|
+
result = previews_controller.previews
|
44
|
+
|
45
|
+
@rendered_content = result
|
46
|
+
|
47
|
+
Nokogiri::HTML.fragment(@rendered_content)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|