view_component 2.52.0 → 2.62.0
Sign up to get free protection for your applications and to get access to all the features.
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
|