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.

@@ -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