view_component 2.49.1 → 3.23.2

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/app/assets/vendor/prism.css +3 -195
  4. data/app/assets/vendor/prism.min.js +11 -11
  5. data/app/controllers/concerns/view_component/preview_actions.rb +108 -0
  6. data/app/controllers/view_components_controller.rb +1 -87
  7. data/app/controllers/view_components_system_test_controller.rb +30 -0
  8. data/app/helpers/preview_helper.rb +30 -12
  9. data/app/views/view_components/_preview_source.html.erb +3 -3
  10. data/app/views/view_components/preview.html.erb +2 -2
  11. data/docs/CHANGELOG.md +1653 -24
  12. data/lib/rails/generators/abstract_generator.rb +16 -10
  13. data/lib/rails/generators/component/component_generator.rb +8 -4
  14. data/lib/rails/generators/component/templates/component.rb.tt +3 -2
  15. data/lib/rails/generators/erb/component_generator.rb +1 -1
  16. data/lib/rails/generators/locale/component_generator.rb +4 -4
  17. data/lib/rails/generators/preview/component_generator.rb +17 -3
  18. data/lib/rails/generators/preview/templates/component_preview.rb.tt +5 -1
  19. data/lib/rails/generators/rspec/component_generator.rb +15 -3
  20. data/lib/rails/generators/rspec/templates/component_spec.rb.tt +3 -1
  21. data/lib/rails/generators/stimulus/component_generator.rb +8 -3
  22. data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
  23. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +3 -1
  24. data/lib/view_component/base.rb +352 -196
  25. data/lib/view_component/capture_compatibility.rb +44 -0
  26. data/lib/view_component/collection.rb +28 -9
  27. data/lib/view_component/compiler.rb +162 -193
  28. data/lib/view_component/config.rb +225 -0
  29. data/lib/view_component/configurable.rb +17 -0
  30. data/lib/view_component/deprecation.rb +8 -0
  31. data/lib/view_component/engine.rb +74 -47
  32. data/lib/view_component/errors.rb +240 -0
  33. data/lib/view_component/inline_template.rb +55 -0
  34. data/lib/view_component/instrumentation.rb +10 -2
  35. data/lib/view_component/preview.rb +21 -19
  36. data/lib/view_component/rails/tasks/view_component.rake +11 -2
  37. data/lib/view_component/render_component_helper.rb +1 -0
  38. data/lib/view_component/render_component_to_string_helper.rb +1 -1
  39. data/lib/view_component/render_to_string_monkey_patch.rb +1 -1
  40. data/lib/view_component/rendering_component_helper.rb +1 -1
  41. data/lib/view_component/rendering_monkey_patch.rb +1 -1
  42. data/lib/view_component/slot.rb +119 -1
  43. data/lib/view_component/slotable.rb +393 -96
  44. data/lib/view_component/slotable_default.rb +20 -0
  45. data/lib/view_component/system_test_case.rb +13 -0
  46. data/lib/view_component/system_test_helpers.rb +27 -0
  47. data/lib/view_component/template.rb +134 -0
  48. data/lib/view_component/test_helpers.rb +208 -47
  49. data/lib/view_component/translatable.rb +51 -33
  50. data/lib/view_component/use_helpers.rb +42 -0
  51. data/lib/view_component/version.rb +5 -4
  52. data/lib/view_component/with_content_helper.rb +3 -8
  53. data/lib/view_component.rb +7 -12
  54. metadata +339 -57
  55. data/lib/rails/generators/component/USAGE +0 -13
  56. data/lib/view_component/content_areas.rb +0 -57
  57. data/lib/view_component/polymorphic_slots.rb +0 -73
  58. data/lib/view_component/preview_template_error.rb +0 -6
  59. data/lib/view_component/previewable.rb +0 -62
  60. data/lib/view_component/slot_v2.rb +0 -104
  61. data/lib/view_component/slotable_v2.rb +0 -307
  62. data/lib/view_component/template_error.rb +0 -9
  63. data/lib/yard/mattr_accessor_handler.rb +0 -19
@@ -0,0 +1,240 @@
1
+ module ViewComponent
2
+ class BaseError < StandardError
3
+ def initialize
4
+ super(self.class::MESSAGE)
5
+ end
6
+ end
7
+
8
+ class DuplicateSlotContentError < StandardError
9
+ MESSAGE =
10
+ "It looks like a block was provided after calling `with_content` on COMPONENT, " \
11
+ "which means that ViewComponent doesn't know which content to use.\n\n" \
12
+ "To fix this issue, use either `with_content` or a block."
13
+
14
+ def initialize(klass_name)
15
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
16
+ end
17
+ end
18
+
19
+ class TemplateError < StandardError
20
+ def initialize(errors, templates = nil)
21
+ message = errors.join("\n")
22
+
23
+ super(message)
24
+ end
25
+ end
26
+
27
+ class MultipleInlineTemplatesError < BaseError
28
+ MESSAGE = "Inline templates can only be defined once per-component."
29
+ end
30
+
31
+ class MissingPreviewTemplateError < StandardError
32
+ MESSAGE =
33
+ "A preview template for example EXAMPLE doesn't exist.\n\n" \
34
+ "To fix this issue, create a template for the example."
35
+
36
+ def initialize(example)
37
+ super(MESSAGE.gsub("EXAMPLE", example))
38
+ end
39
+ end
40
+
41
+ class DuplicateContentError < StandardError
42
+ MESSAGE =
43
+ "It looks like a block was provided after calling `with_content` on COMPONENT, " \
44
+ "which means that ViewComponent doesn't know which content to use.\n\n" \
45
+ "To fix this issue, use either `with_content` or a block."
46
+
47
+ def initialize(klass_name)
48
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
49
+ end
50
+ end
51
+
52
+ class EmptyOrInvalidInitializerError < StandardError
53
+ MESSAGE =
54
+ "The COMPONENT initializer is empty or invalid. " \
55
+ "It must accept the parameter `PARAMETER` to render it as a collection.\n\n" \
56
+ "To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
57
+ "See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
58
+
59
+ def initialize(klass_name, parameter)
60
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
61
+ end
62
+ end
63
+
64
+ class MissingCollectionArgumentError < StandardError
65
+ MESSAGE =
66
+ "The initializer for COMPONENT doesn't accept the parameter `PARAMETER`, " \
67
+ "which is required to render it as a collection.\n\n" \
68
+ "To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
69
+ "See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
70
+
71
+ def initialize(klass_name, parameter)
72
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
73
+ end
74
+ end
75
+
76
+ class ReservedParameterError < StandardError
77
+ MESSAGE =
78
+ "COMPONENT initializer can't accept the parameter `PARAMETER`, as it will override a " \
79
+ "public ViewComponent method. To fix this issue, rename the parameter."
80
+
81
+ def initialize(klass_name, parameter)
82
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
83
+ end
84
+ end
85
+
86
+ class InvalidCollectionArgumentError < BaseError
87
+ MESSAGE =
88
+ "The value of the first argument passed to `with_collection` isn't a valid collection. " \
89
+ "Make sure it responds to `to_ary`."
90
+ end
91
+
92
+ class ContentSlotNameError < StandardError
93
+ MESSAGE =
94
+ "COMPONENT declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
95
+ "Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor without having to create an explicit slot.\n\n" \
96
+ "To fix this issue, either use the `content` accessor directly or choose a different slot name."
97
+
98
+ def initialize(klass_name)
99
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
100
+ end
101
+ end
102
+
103
+ class InvalidSlotDefinitionError < BaseError
104
+ MESSAGE =
105
+ "Invalid slot definition. Please pass a class, " \
106
+ "string, or callable (that is proc, lambda, etc)"
107
+ end
108
+
109
+ class InvalidSlotNameError < StandardError
110
+ end
111
+
112
+ class SlotPredicateNameError < InvalidSlotNameError
113
+ MESSAGE =
114
+ "COMPONENT declares a slot named SLOT_NAME, which ends with a question mark.\n\n" \
115
+ "This isn't allowed because the ViewComponent framework already provides predicate " \
116
+ "methods ending in `?`.\n\n" \
117
+ "To fix this issue, choose a different name."
118
+
119
+ def initialize(klass_name, slot_name)
120
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
121
+ end
122
+ end
123
+
124
+ class RedefinedSlotError < StandardError
125
+ MESSAGE =
126
+ "COMPONENT declares the SLOT_NAME slot multiple times.\n\n" \
127
+ "To fix this issue, choose a different slot name."
128
+
129
+ def initialize(klass_name, slot_name)
130
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
131
+ end
132
+ end
133
+
134
+ class ReservedSingularSlotNameError < InvalidSlotNameError
135
+ MESSAGE =
136
+ "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
137
+ "To fix this issue, choose a different name."
138
+
139
+ def initialize(klass_name, slot_name)
140
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
141
+ end
142
+ end
143
+
144
+ class ReservedPluralSlotNameError < InvalidSlotNameError
145
+ MESSAGE =
146
+ "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
147
+ "To fix this issue, choose a different name."
148
+
149
+ def initialize(klass_name, slot_name)
150
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
151
+ end
152
+ end
153
+
154
+ class UncountableSlotNameError < InvalidSlotNameError
155
+ MESSAGE =
156
+ "COMPONENT declares a slot named SLOT_NAME, which is an uncountable word\n\n" \
157
+ "To fix this issue, choose a different name."
158
+
159
+ def initialize(klass_name, slot_name)
160
+ super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
161
+ end
162
+ end
163
+
164
+ class ContentAlreadySetForPolymorphicSlotError < StandardError
165
+ MESSAGE = "Content for slot SLOT_NAME has already been provided."
166
+
167
+ def initialize(slot_name)
168
+ super(MESSAGE.gsub("SLOT_NAME", slot_name.to_s))
169
+ end
170
+ end
171
+
172
+ class NilWithContentError < BaseError
173
+ MESSAGE =
174
+ "No content provided to `#with_content` for #{self}.\n\n" \
175
+ "To fix this issue, pass a value."
176
+ end
177
+
178
+ class TranslateCalledBeforeRenderError < BaseError
179
+ MESSAGE =
180
+ "`#translate` can't be used before rendering as it depends " \
181
+ "on the view context that only exists once a ViewComponent is passed to " \
182
+ "the Rails render pipeline.\n\n" \
183
+ "It's sometimes possible to fix this issue by moving code dependent on " \
184
+ "`#translate` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
185
+ end
186
+
187
+ class HelpersCalledBeforeRenderError < BaseError
188
+ MESSAGE =
189
+ "`#helpers` can't be used before rendering as it depends " \
190
+ "on the view context that only exists once a ViewComponent is passed to " \
191
+ "the Rails render pipeline.\n\n" \
192
+ "It's sometimes possible to fix this issue by moving code dependent on " \
193
+ "`#helpers` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
194
+ end
195
+
196
+ class ControllerCalledBeforeRenderError < BaseError
197
+ MESSAGE =
198
+ "`#controller` can't be used before rendering, as it depends " \
199
+ "on the view context that only exists once a ViewComponent is passed to " \
200
+ "the Rails render pipeline.\n\n" \
201
+ "It's sometimes possible to fix this issue by moving code dependent on " \
202
+ "`#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
203
+ end
204
+
205
+ # :nocov:
206
+ class NoMatchingTemplatesForPreviewError < StandardError
207
+ MESSAGE = "Found 0 matches for templates for TEMPLATE_IDENTIFIER."
208
+
209
+ def initialize(template_identifier)
210
+ super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier))
211
+ end
212
+ end
213
+
214
+ class MultipleMatchingTemplatesForPreviewError < StandardError
215
+ MESSAGE = "Found multiple templates for TEMPLATE_IDENTIFIER."
216
+
217
+ def initialize(template_identifier)
218
+ super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier))
219
+ end
220
+ end
221
+ # :nocov:
222
+
223
+ class SystemTestControllerOnlyAllowedInTestError < BaseError
224
+ MESSAGE = "ViewComponent SystemTest controller must only be called in a test environment for security reasons."
225
+ end
226
+
227
+ class SystemTestControllerNefariousPathError < BaseError
228
+ MESSAGE = "ViewComponent SystemTest controller attempted to load a file outside of the expected directory."
229
+ end
230
+
231
+ class AlreadyDefinedPolymorphicSlotSetterError < StandardError
232
+ MESSAGE =
233
+ "A method called 'SETTER_METHOD_NAME' already exists and would be overwritten by the 'SETTER_NAME' polymorphic " \
234
+ "slot setter.\n\nPlease choose a different setter name."
235
+
236
+ def initialize(setter_method_name, setter_name)
237
+ super(MESSAGE.gsub("SETTER_METHOD_NAME", setter_method_name.to_s).gsub("SETTER_NAME", setter_name.to_s))
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent # :nodoc:
4
+ module InlineTemplate
5
+ extend ActiveSupport::Concern
6
+ Template = Struct.new(:source, :language, :path, :lineno)
7
+
8
+ class_methods do
9
+ def method_missing(method, *args)
10
+ return super if !method.end_with?("_template")
11
+
12
+ if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
13
+ raise MultipleInlineTemplatesError
14
+ end
15
+
16
+ if args.size != 1
17
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
18
+ end
19
+
20
+ ext = method.to_s.gsub("_template", "")
21
+ template = args.first
22
+
23
+ @__vc_inline_template_language = ext
24
+
25
+ caller = caller_locations(1..1)[0]
26
+ @__vc_inline_template = Template.new(
27
+ template,
28
+ ext,
29
+ caller.absolute_path || caller.path,
30
+ caller.lineno
31
+ )
32
+
33
+ @__vc_inline_template_defined = true
34
+ end
35
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
36
+
37
+ def respond_to_missing?(method, include_all = false)
38
+ method.end_with?("_template") || super
39
+ end
40
+
41
+ def inline_template
42
+ @__vc_inline_template if defined?(@__vc_inline_template)
43
+ end
44
+
45
+ def inline_template_language
46
+ @__vc_inline_template_language if defined?(@__vc_inline_template_language)
47
+ end
48
+
49
+ def inherited(subclass)
50
+ super
51
+ subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -10,14 +10,22 @@ module ViewComponent # :nodoc:
10
10
 
11
11
  def render_in(view_context, &block)
12
12
  ActiveSupport::Notifications.instrument(
13
- "!render.view_component",
13
+ notification_name,
14
14
  {
15
15
  name: self.class.name,
16
16
  identifier: self.class.identifier
17
17
  }
18
18
  ) do
19
- super(view_context, &block)
19
+ super
20
20
  end
21
21
  end
22
+
23
+ private
24
+
25
+ def notification_name
26
+ return "!render.view_component" if ViewComponent::Base.config.use_deprecated_instrumentation_name
27
+
28
+ "render.view_component"
29
+ end
22
30
  end
23
31
  end
@@ -4,6 +4,11 @@ require "active_support/descendants_tracker"
4
4
 
5
5
  module ViewComponent # :nodoc:
6
6
  class Preview
7
+ if defined?(Rails.application.routes.url_helpers)
8
+ # Workaround from https://stackoverflow.com/questions/20853526/make-yard-ignore-certain-class-extensions to appease YARD
9
+ send(:include, Rails.application.routes.url_helpers)
10
+ end
11
+
7
12
  include ActionView::Helpers::TagHelper
8
13
  include ActionView::Helpers::AssetTagHelper
9
14
  extend ActiveSupport::DescendantsTracker
@@ -14,7 +19,7 @@ module ViewComponent # :nodoc:
14
19
  block: block,
15
20
  component: component,
16
21
  locals: {},
17
- template: "view_components/preview",
22
+ template: "view_components/preview"
18
23
  }
19
24
  end
20
25
 
@@ -30,7 +35,8 @@ module ViewComponent # :nodoc:
30
35
  class << self
31
36
  # Returns all component preview classes.
32
37
  def all
33
- load_previews if descendants.empty?
38
+ load_previews
39
+
34
40
  descendants
35
41
  end
36
42
 
@@ -65,47 +71,43 @@ module ViewComponent # :nodoc:
65
71
  name.chomp("Preview").underscore
66
72
  end
67
73
 
74
+ # rubocop:disable Style/TrivialAccessors
68
75
  # Setter for layout name.
69
76
  def layout(layout_name)
70
77
  @layout = layout_name
71
78
  end
79
+ # rubocop:enable Style/TrivialAccessors
72
80
 
73
81
  # Returns the relative path (from preview_path) to the preview example template if the template exists
74
82
  def preview_example_template_path(example)
75
83
  preview_path =
76
- Array(preview_paths).detect do |preview_path|
77
- Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
84
+ Array(preview_paths).detect do |path|
85
+ Dir["#{path}/#{preview_name}_preview/#{example}.html.*"].first
78
86
  end
79
87
 
80
- if preview_path.nil?
81
- raise(
82
- PreviewTemplateError,
83
- "A preview template for example #{example} doesn't exist.\n\n" \
84
- "To fix this issue, create a template for the example."
85
- )
86
- end
88
+ raise MissingPreviewTemplateError.new(example) if preview_path.nil?
87
89
 
88
90
  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(/\..*$/, "")
91
+ Pathname.new(path)
92
+ .relative_path_from(Pathname.new(preview_path))
93
+ .to_s
94
+ .sub(/\..*$/, "")
93
95
  end
94
96
 
95
97
  # Returns the method body for the example from the preview file.
96
98
  def preview_source(example)
97
- source = self.instance_method(example.to_sym).source.split("\n")
99
+ source = instance_method(example.to_sym).source.split("\n")
98
100
  source[1...(source.size - 1)].join("\n")
99
101
  end
100
102
 
101
- private
102
-
103
103
  def load_previews
104
104
  Array(preview_paths).each do |preview_path|
105
- Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
105
+ Dir["#{preview_path}/**/*preview.rb"].sort.each { |file| require_dependency file }
106
106
  end
107
107
  end
108
108
 
109
+ private
110
+
109
111
  def preview_paths
110
112
  Base.preview_paths
111
113
  end
@@ -3,9 +3,18 @@
3
3
  task stats: "view_component:statsetup"
4
4
 
5
5
  namespace :view_component do
6
- task statsetup: :environment do
6
+ task :statsetup do
7
+ # :nocov:
7
8
  require "rails/code_statistics"
8
9
 
9
- ::STATS_DIRECTORIES << ["ViewComponents", ViewComponent::Base.view_component_path]
10
+ if Rails.root.join(ViewComponent::Base.view_component_path).directory?
11
+ ::STATS_DIRECTORIES << ["ViewComponents", ViewComponent::Base.view_component_path]
12
+ end
13
+
14
+ if Rails.root.join("test/components").directory?
15
+ ::STATS_DIRECTORIES << ["ViewComponent tests", "test/components"]
16
+ CodeStatistics::TEST_TYPES << "ViewComponent tests"
17
+ end
18
+ # :nocov:
10
19
  end
11
20
  end
@@ -3,6 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderComponentHelper # :nodoc:
5
5
  def render_component(component, &block)
6
+ component.set_original_view_context(__vc_original_view_context) if is_a?(ViewComponent::Base)
6
7
  component.render_in(self, &block)
7
8
  end
8
9
  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
@@ -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
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderingComponentHelper # :nodoc:
5
5
  def render_component(component)
6
- self.response_body = component.render_in(self.view_context)
6
+ self.response_body = component.render_in(view_context)
7
7
  end
8
8
  end
9
9
  end
@@ -4,7 +4,7 @@ module ViewComponent
4
4
  module RenderingMonkeyPatch # :nodoc:
5
5
  def render(options = {}, args = {})
6
6
  if options.respond_to?(:render_in)
7
- self.response_body = options.render_in(self.view_context)
7
+ self.response_body = options.render_in(view_context)
8
8
  else
9
9
  super
10
10
  end
@@ -1,7 +1,125 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "view_component/with_content_helper"
4
+
3
5
  module ViewComponent
4
6
  class Slot
5
- attr_accessor :content
7
+ include ViewComponent::WithContentHelper
8
+
9
+ attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content
10
+
11
+ def initialize(parent)
12
+ @parent = parent
13
+ end
14
+
15
+ def content?
16
+ return true if defined?(@__vc_content) && @__vc_content.present?
17
+ return true if defined?(@__vc_content_set_by_with_content) && @__vc_content_set_by_with_content.present?
18
+ return true if defined?(@__vc_content_block) && @__vc_content_block.present?
19
+ return false if !__vc_component_instance?
20
+
21
+ @__vc_component_instance.content?
22
+ end
23
+
24
+ def with_content(args)
25
+ if __vc_component_instance?
26
+ @__vc_component_instance.with_content(args)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ # Used to render the slot content in the template
33
+ #
34
+ # There's currently 3 different values that may be set, that we can render.
35
+ #
36
+ # If the slot renderable is a component, the string class name of a
37
+ # component, or a function that returns a component, we render that
38
+ # component instance, returning the string.
39
+ #
40
+ # If the slot renderable is a function and returns a string, it's
41
+ # set as `@__vc_content` and is returned directly.
42
+ #
43
+ # If there is no slot renderable, we evaluate the block passed to
44
+ # the slot and return it.
45
+ def to_s
46
+ return @content if defined?(@content)
47
+
48
+ view_context = @parent.send(:view_context)
49
+
50
+ if defined?(@__vc_content_block) && defined?(@__vc_content_set_by_with_content)
51
+ raise DuplicateSlotContentError.new(self.class.name)
52
+ end
53
+
54
+ @content =
55
+ if __vc_component_instance?
56
+ @__vc_component_instance.__vc_original_view_context = @parent.__vc_original_view_context
57
+
58
+ if defined?(@__vc_content_block)
59
+ # render_in is faster than `parent.render`
60
+ @__vc_component_instance.render_in(view_context) do |*args|
61
+ return @__vc_content_block.call(*args) if @__vc_content_block&.source_location.nil?
62
+
63
+ block_context = @__vc_content_block.binding.receiver
64
+
65
+ if block_context.class < ActionView::Base
66
+ block_context.capture(*args, &@__vc_content_block)
67
+ else
68
+ @__vc_content_block.call(*args)
69
+ end
70
+ end
71
+ else
72
+ @__vc_component_instance.render_in(view_context)
73
+ end
74
+ elsif defined?(@__vc_content)
75
+ @__vc_content
76
+ elsif defined?(@__vc_content_block)
77
+ view_context.capture(&@__vc_content_block)
78
+ elsif defined?(@__vc_content_set_by_with_content)
79
+ @__vc_content_set_by_with_content
80
+ end
81
+
82
+ @content = @content.to_s
83
+ end
84
+
85
+ # Allow access to public component methods via the wrapper
86
+ #
87
+ # for example
88
+ #
89
+ # calling `header.name` (where `header` is a slot) will call `name`
90
+ # on the `HeaderComponent` instance.
91
+ #
92
+ # Where the component may look like:
93
+ #
94
+ # class MyComponent < ViewComponent::Base
95
+ # has_one :header, HeaderComponent
96
+ #
97
+ # class HeaderComponent < ViewComponent::Base
98
+ # def name
99
+ # @name
100
+ # end
101
+ # end
102
+ # end
103
+ #
104
+ def method_missing(symbol, *args, &block)
105
+ @__vc_component_instance.public_send(symbol, *args, &block)
106
+ end
107
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
108
+
109
+ def html_safe?
110
+ # :nocov:
111
+ to_s.html_safe?
112
+ # :nocov:
113
+ end
114
+
115
+ def respond_to_missing?(symbol, include_all = false)
116
+ __vc_component_instance? && @__vc_component_instance.respond_to?(symbol, include_all)
117
+ end
118
+
119
+ private
120
+
121
+ def __vc_component_instance?
122
+ defined?(@__vc_component_instance)
123
+ end
6
124
  end
7
125
  end