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.

@@ -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 unless @global_buffer_in_use
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
- perform_render
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
- def perform_render
116
- render_template_for(@__vc_variant).to_s + _output_postamble
117
- end
118
-
119
- # :nocov:
120
- def render_template_for(variant = nil)
121
- # Force compilation here so the compiler always redefines render_template_for.
122
- # This is mostly a safeguard to prevent infinite recursion.
123
- self.class.compile(raise_errors: true, force: true)
124
- # .compile replaces this method; call the new one
125
- render_template_for(variant)
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(*); end
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.is_a? ViewComponent::Base
171
- options.__vc_original_view_context = __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
- # config.view_component.test_controller = "MyTestController"
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
- # config.view_component.render_monkey_patch_enabled = false
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
- # config.view_component.view_component_path = "app/my_components"
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
- # config.view_component.component_parent_class = "MyBaseComponent"
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
- # config.view_component.generate.sidecar = true
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
- # config.view_component.generate.stimulus_controller = true
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
- # config.view_component.generate.locale = true
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
- # config.view_component.generate.distinct_locale_files = true
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
- # config.view_component.generate.preview = true
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
- # render(ProductsComponent.with_collection(@products, foo: :bar))
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
- # with_collection_parameter :item
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
- if provided_collection_parameter
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 # rubocop:disable Rails/OutputSafety
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 = { component.collection_parameter => item }
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
 
@@ -20,11 +20,10 @@ module ViewComponent
20
20
 
21
21
  def invalidate_class!(klass)
22
22
  cache.delete(klass)
23
- klass.compiler.reset_render_template_for
24
23
  end
25
24
 
26
25
  def invalidate!
27
- cache.each { |klass| invalidate_class!(klass) }
26
+ cache.clear
28
27
  end
29
28
  end
30
29
  end
@@ -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(false).include?(method_name.to_sym)
68
- component_class.send(:remove_method, method_name.to_sym)
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
- reset_render_template_for
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
- group_by { |template| template[:variant] }.
155
- map { |variant, grouped| variant if grouped.length > 1 }.
156
- compact.
157
- sort
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 #{'variant'.pluralize(invalid_variants.count)} " \
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 #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} " \
180
- "found for #{'variant'.pluralize(count)} " \
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).gsub(".", ""))
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
@@ -21,7 +21,7 @@ module ViewComponent
21
21
  )
22
22
  end
23
23
 
24
- if block_given?
24
+ if block
25
25
  content = view_context.capture(&block)
26
26
  end
27
27
 
@@ -28,7 +28,7 @@ module ViewComponent
28
28
  end
29
29
 
30
30
  def types
31
- " → [#{@method.tag(:return).types.join(',')}]" if @method.tag(:return)&.types && show_types?
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.concat(options.preview_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
- ViewComponent::Compiler::DEVELOPMENT_MODE
137
- else
138
- ViewComponent::Compiler::PRODUCTION_MODE
139
- end
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
- self.registered_slots[slot_name] = {
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 if descendants.empty?
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 = self.instance_method(example.to_sym).source.split("\n")
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
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderComponentToStringHelper # :nodoc:
5
5
  def render_component_to_string(component)
6
- component.render_in(self.view_context)
6
+ component.render_in(view_context)
7
7
  end
8
8
  end
9
9
  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
@@ -4,7 +4,7 @@ module ViewComponent
4
4
  module RenderToStringMonkeyPatch # :nodoc:
5
5
  def render_to_string(options = {}, args = {})
6
6
  if options.respond_to?(:render_in)
7
- options.render_in(self.view_context)
7
+ options.render_in(view_context)
8
8
  else
9
9
  super
10
10
  end