view_component 2.82.0 → 3.1.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.

Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/view_component/preview_actions.rb +4 -0
  3. data/app/controllers/view_components_system_test_controller.rb +24 -1
  4. data/app/helpers/preview_helper.rb +2 -4
  5. data/docs/CHANGELOG.md +320 -0
  6. data/lib/view_component/base.rb +50 -96
  7. data/lib/view_component/capture_compatibility.rb +44 -0
  8. data/lib/view_component/collection.rb +2 -5
  9. data/lib/view_component/compiler.rb +51 -28
  10. data/lib/view_component/config.rb +9 -13
  11. data/lib/view_component/deprecation.rb +1 -1
  12. data/lib/view_component/docs_builder_component.html.erb +5 -1
  13. data/lib/view_component/docs_builder_component.rb +28 -9
  14. data/lib/view_component/engine.rb +12 -22
  15. data/lib/view_component/errors.rb +223 -0
  16. data/lib/view_component/inline_template.rb +55 -0
  17. data/lib/view_component/preview.rb +1 -7
  18. data/lib/view_component/rails/tasks/view_component.rake +1 -1
  19. data/lib/view_component/slot.rb +109 -1
  20. data/lib/view_component/slotable.rb +364 -96
  21. data/lib/view_component/system_test_helpers.rb +5 -5
  22. data/lib/view_component/test_helpers.rb +67 -54
  23. data/lib/view_component/translatable.rb +35 -23
  24. data/lib/view_component/version.rb +4 -3
  25. data/lib/view_component/with_content_helper.rb +3 -8
  26. data/lib/view_component.rb +3 -12
  27. metadata +45 -34
  28. data/lib/view_component/content_areas.rb +0 -56
  29. data/lib/view_component/polymorphic_slots.rb +0 -103
  30. data/lib/view_component/preview_template_error.rb +0 -6
  31. data/lib/view_component/slot_v2.rb +0 -98
  32. data/lib/view_component/slotable_v2.rb +0 -391
  33. data/lib/view_component/template_error.rb +0 -9
@@ -80,13 +80,7 @@ module ViewComponent # :nodoc:
80
80
  Dir["#{path}/#{preview_name}_preview/#{example}.html.*"].first
81
81
  end
82
82
 
83
- if preview_path.nil?
84
- raise(
85
- PreviewTemplateError,
86
- "A preview template for example #{example} doesn't exist.\n\n" \
87
- "To fix this issue, create a template for the example."
88
- )
89
- end
83
+ raise MissingPreviewTemplateError.new(example) if preview_path.nil?
90
84
 
91
85
  path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
92
86
  Pathname.new(path)
@@ -3,7 +3,7 @@
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
7
  require "rails/code_statistics"
8
8
 
9
9
  ::STATS_DIRECTORIES << ["ViewComponents", ViewComponent::Base.view_component_path]
@@ -1,7 +1,115 @@
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, &@__vc_content_block)
61
+ else
62
+ @__vc_component_instance.render_in(view_context)
63
+ end
64
+ elsif defined?(@__vc_content)
65
+ @__vc_content
66
+ elsif defined?(@__vc_content_block)
67
+ view_context.capture(&@__vc_content_block)
68
+ elsif defined?(@__vc_content_set_by_with_content)
69
+ @__vc_content_set_by_with_content
70
+ end
71
+
72
+ @content = @content.to_s
73
+ end
74
+
75
+ # Allow access to public component methods via the wrapper
76
+ #
77
+ # for example
78
+ #
79
+ # calling `header.name` (where `header` is a slot) will call `name`
80
+ # on the `HeaderComponent` instance.
81
+ #
82
+ # Where the component may look like:
83
+ #
84
+ # class MyComponent < ViewComponent::Base
85
+ # has_one :header, HeaderComponent
86
+ #
87
+ # class HeaderComponent < ViewComponent::Base
88
+ # def name
89
+ # @name
90
+ # end
91
+ # end
92
+ # end
93
+ #
94
+ def method_missing(symbol, *args, &block)
95
+ @__vc_component_instance.public_send(symbol, *args, &block)
96
+ end
97
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
98
+
99
+ def html_safe?
100
+ # :nocov:
101
+ to_s.html_safe?
102
+ # :nocov:
103
+ end
104
+
105
+ def respond_to_missing?(symbol, include_all = false)
106
+ __vc_component_instance? && @__vc_component_instance.respond_to?(symbol, include_all)
107
+ end
108
+
109
+ private
110
+
111
+ def __vc_component_instance?
112
+ defined?(@__vc_component_instance)
113
+ end
6
114
  end
7
115
  end
@@ -1,145 +1,413 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
-
5
4
  require "view_component/slot"
6
5
 
7
6
  module ViewComponent
8
7
  module Slotable
9
8
  extend ActiveSupport::Concern
10
9
 
10
+ RESERVED_NAMES = {
11
+ singular: %i[content render].freeze,
12
+ plural: %i[contents renders].freeze
13
+ }.freeze
14
+
15
+ # Setup component slot state
11
16
  included do
12
17
  # Hash of registered Slots
13
- class_attribute :slots
14
- self.slots = {}
18
+ class_attribute :registered_slots
19
+ self.registered_slots = {}
15
20
  end
16
21
 
17
22
  class_methods do
18
- # support initializing slots as:
19
- #
20
- # with_slot(
21
- # :header,
22
- # collection: true|false,
23
- # class_name: "Header" # class name string, used to instantiate Slot
24
- # )
25
- def with_slot(*slot_names, collection: false, class_name: nil)
26
- ViewComponent::Deprecation.deprecation_warning(
27
- "`with_slot`", "use the new slots API (https://viewcomponent.org/guide/slots.html) instead"
28
- )
29
-
30
- slot_names.each do |slot_name|
31
- # Ensure slot_name isn't already declared
32
- if slots.key?(slot_name)
33
- raise ArgumentError.new("#{slot_name} slot declared multiple times")
23
+ ##
24
+ # Registers a sub-component
25
+ #
26
+ # = Example
27
+ #
28
+ # renders_one :header -> (classes:) do
29
+ # HeaderComponent.new(classes: classes)
30
+ # end
31
+ #
32
+ # # OR
33
+ #
34
+ # renders_one :header, HeaderComponent
35
+ #
36
+ # where `HeaderComponent` is defined as:
37
+ #
38
+ # class HeaderComponent < ViewComponent::Base
39
+ # def initialize(classes:)
40
+ # @classes = classes
41
+ # end
42
+ # end
43
+ #
44
+ # and has the following template:
45
+ #
46
+ # <header class="<%= @classes %>">
47
+ # <%= content %>
48
+ # </header>
49
+ #
50
+ # = Rendering sub-component content
51
+ #
52
+ # The component's sidecar template can access the sub-component by calling a
53
+ # helper method with the same name as the sub-component.
54
+ #
55
+ # <h1>
56
+ # <%= header do %>
57
+ # My header title
58
+ # <% end %>
59
+ # </h1>
60
+ #
61
+ # = Setting sub-component content
62
+ #
63
+ # Consumers of the component can render a sub-component by calling a
64
+ # helper method with the same name as the slot prefixed with `with_`.
65
+ #
66
+ # <%= render_inline(MyComponent.new) do |component| %>
67
+ # <% component.with_header(classes: "Foo") do %>
68
+ # <p>Bar</p>
69
+ # <% end %>
70
+ # <% end %>
71
+ #
72
+ # Additionally, content can be set by calling `with_SLOT_NAME_content`
73
+ # on the component instance.
74
+ #
75
+ # <%= render_inline(MyComponent.new.with_header_content("Foo")) %>
76
+ def renders_one(slot_name, callable = nil)
77
+ validate_singular_slot_name(slot_name)
78
+
79
+ if callable.is_a?(Hash) && callable.key?(:types)
80
+ register_polymorphic_slot(slot_name, callable[:types], collection: false)
81
+ else
82
+ validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
83
+
84
+ setter_method_name = :"with_#{slot_name}"
85
+
86
+ define_method setter_method_name do |*args, &block|
87
+ set_slot(slot_name, nil, *args, &block)
88
+ end
89
+ ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
90
+
91
+ define_method slot_name do
92
+ get_slot(slot_name)
93
+ end
94
+
95
+ define_method "#{slot_name}?" do
96
+ get_slot(slot_name).present?
97
+ end
98
+
99
+ define_method "with_#{slot_name}_content" do |content|
100
+ send(setter_method_name) { content.to_s }
101
+
102
+ self
103
+ end
104
+
105
+ register_slot(slot_name, collection: false, callable: callable)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Registers a collection sub-component
111
+ #
112
+ # = Example
113
+ #
114
+ # renders_many :items, -> (name:) { ItemComponent.new(name: name }
115
+ #
116
+ # # OR
117
+ #
118
+ # renders_many :items, ItemComponent
119
+ #
120
+ # = Rendering sub-components
121
+ #
122
+ # The component's sidecar template can access the slot by calling a
123
+ # helper method with the same name as the slot.
124
+ #
125
+ # <h1>
126
+ # <% items.each do |item| %>
127
+ # <%= item %>
128
+ # <% end %>
129
+ # </h1>
130
+ #
131
+ # = Setting sub-component content
132
+ #
133
+ # Consumers of the component can set the content of a slot by calling a
134
+ # helper method with the same name as the slot prefixed with `with_`. The
135
+ # method can be called multiple times to append to the slot.
136
+ #
137
+ # <%= render_inline(MyComponent.new) do |component| %>
138
+ # <% component.with_item(name: "Foo") do %>
139
+ # <p>One</p>
140
+ # <% end %>
141
+ #
142
+ # <% component.with_item(name: "Bar") do %>
143
+ # <p>two</p>
144
+ # <% end %>
145
+ # <% end %>
146
+ def renders_many(slot_name, callable = nil)
147
+ validate_plural_slot_name(slot_name)
148
+
149
+ if callable.is_a?(Hash) && callable.key?(:types)
150
+ register_polymorphic_slot(slot_name, callable[:types], collection: true)
151
+ else
152
+ singular_name = ActiveSupport::Inflector.singularize(slot_name)
153
+ validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
154
+
155
+ setter_method_name = :"with_#{singular_name}"
156
+
157
+ define_method setter_method_name do |*args, &block|
158
+ set_slot(slot_name, nil, *args, &block)
159
+ end
160
+ ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
161
+
162
+ define_method "with_#{singular_name}_content" do |content|
163
+ send(setter_method_name) { content.to_s }
164
+
165
+ self
34
166
  end
35
167
 
36
- # Ensure slot name isn't :content
37
- if slot_name == :content
38
- raise ArgumentError.new ":content is a reserved slot name. Please use another name, such as ':body'"
168
+ define_method :"with_#{slot_name}" do |collection_args = nil, &block|
169
+ collection_args.map do |args|
170
+ if args.respond_to?(:to_hash)
171
+ set_slot(slot_name, nil, **args, &block)
172
+ else
173
+ set_slot(slot_name, nil, *args, &block)
174
+ end
175
+ end
176
+ end
177
+
178
+ define_method slot_name do
179
+ get_slot(slot_name)
180
+ end
181
+
182
+ define_method "#{slot_name}?" do
183
+ get_slot(slot_name).present?
184
+ end
185
+
186
+ register_slot(slot_name, collection: true, callable: callable)
187
+ end
188
+ end
189
+
190
+ def slot_type(slot_name)
191
+ registered_slot = registered_slots[slot_name]
192
+ if registered_slot
193
+ registered_slot[:collection] ? :collection : :single
194
+ else
195
+ plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym
196
+ plural_registered_slot = registered_slots[plural_slot_name]
197
+ plural_registered_slot&.fetch(:collection) ? :collection_item : nil
198
+ end
199
+ end
200
+
201
+ # Clone slot configuration into child class
202
+ # see #test_slots_pollution
203
+ def inherited(child)
204
+ child.registered_slots = registered_slots.clone
205
+ super
206
+ end
207
+
208
+ def register_polymorphic_slot(slot_name, types, collection:)
209
+ define_method(slot_name) do
210
+ get_slot(slot_name)
211
+ end
212
+
213
+ define_method("#{slot_name}?") do
214
+ get_slot(slot_name).present?
215
+ end
216
+
217
+ renderable_hash = types.each_with_object({}) do |(poly_type, poly_attributes_or_callable), memo|
218
+ if poly_attributes_or_callable.is_a?(Hash)
219
+ poly_callable = poly_attributes_or_callable[:renders]
220
+ poly_slot_name = poly_attributes_or_callable[:as]
221
+ else
222
+ poly_callable = poly_attributes_or_callable
223
+ poly_slot_name = nil
39
224
  end
40
225
 
41
- # Set the name of the method used to access the Slot(s)
42
- accessor_name =
226
+ poly_slot_name ||=
43
227
  if collection
44
- # If Slot is a collection, set the accessor
45
- # name to the pluralized form of the slot name
46
- # For example: :tab => :tabs
47
- ActiveSupport::Inflector.pluralize(slot_name)
228
+ "#{ActiveSupport::Inflector.singularize(slot_name)}_#{poly_type}"
48
229
  else
49
- slot_name
230
+ "#{slot_name}_#{poly_type}"
50
231
  end
51
232
 
52
- instance_variable_name = "@#{accessor_name}"
233
+ memo[poly_type] = define_slot(
234
+ poly_slot_name, collection: collection, callable: poly_callable
235
+ )
53
236
 
54
- # If the slot is a collection, define an accesor that defaults to an empty array
55
- if collection
56
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
57
- def #{accessor_name}
58
- content unless content_evaluated? # ensure content is loaded so slots will be defined
59
- #{instance_variable_name} ||= []
60
- end
61
- RUBY
62
- else
63
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
64
- def #{accessor_name}
65
- content unless content_evaluated? # ensure content is loaded so slots will be defined
66
- #{instance_variable_name} if defined?(#{instance_variable_name})
67
- end
68
- RUBY
237
+ setter_method_name = :"with_#{poly_slot_name}"
238
+
239
+ if instance_methods.include?(setter_method_name)
240
+ raise AlreadyDefinedPolymorphicSlotSetterError.new(setter_method_name, poly_slot_name)
241
+ end
242
+
243
+ define_method(setter_method_name) do |*args, &block|
244
+ set_polymorphic_slot(slot_name, poly_type, *args, &block)
69
245
  end
246
+ ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
70
247
 
71
- # Default class_name to ViewComponent::Slot
72
- class_name = "ViewComponent::Slot" unless class_name.present?
248
+ define_method "with_#{poly_slot_name}_content" do |content|
249
+ send(setter_method_name) { content.to_s }
73
250
 
74
- # Register the slot on the component
75
- slots[slot_name] = {
76
- class_name: class_name,
77
- instance_variable_name: instance_variable_name,
78
- collection: collection
79
- }
251
+ self
252
+ end
80
253
  end
254
+
255
+ registered_slots[slot_name] = {
256
+ collection: collection,
257
+ renderable_hash: renderable_hash
258
+ }
81
259
  end
82
260
 
83
- def inherited(child)
84
- # Clone slot configuration into child class
85
- # see #test_slots_pollution
86
- child.slots = slots.clone
261
+ private
87
262
 
88
- super
263
+ def register_slot(slot_name, **kwargs)
264
+ registered_slots[slot_name] = define_slot(slot_name, **kwargs)
89
265
  end
90
- end
91
266
 
92
- # Build a Slot instance on a component,
93
- # exposing it for use inside the
94
- # component template.
95
- #
96
- # slot: Name of Slot, in symbol form
97
- # **args: Arguments to be passed to Slot initializer
98
- #
99
- # For example:
100
- # <%= render(SlotsComponent.new) do |component| %>
101
- # <% component.slot(:footer, class_names: "footer-class") do %>
102
- # <p>This is my footer!</p>
103
- # <% end %>
104
- # <% end %>
105
- #
106
- def slot(slot_name, **args, &block)
107
- # Raise ArgumentError if `slot` doesn't exist
108
- unless slots.key?(slot_name)
109
- raise ArgumentError.new "Unknown slot '#{slot_name}' - expected one of '#{slots.keys}'"
267
+ def define_slot(slot_name, collection:, callable:)
268
+ # Setup basic slot data
269
+ slot = {
270
+ collection: collection
271
+ }
272
+ return slot unless callable
273
+
274
+ # If callable responds to `render_in`, we set it on the slot as a renderable
275
+ if callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
276
+ slot[:renderable] = callable
277
+ elsif callable.is_a?(String)
278
+ # If callable is a string, we assume it's referencing an internal class
279
+ slot[:renderable_class_name] = callable
280
+ elsif callable.respond_to?(:call)
281
+ # If slot doesn't respond to `render_in`, we assume it's a proc,
282
+ # define a method, and save a reference to it to call when setting
283
+ method_name = :"_call_#{slot_name}"
284
+ define_method method_name, &callable
285
+ slot[:renderable_function] = instance_method(method_name)
286
+ else
287
+ raise(InvalidSlotDefinitionError)
288
+ end
289
+
290
+ slot
291
+ end
292
+
293
+ def validate_plural_slot_name(slot_name)
294
+ if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
295
+ raise ReservedPluralSlotNameError.new(name, slot_name)
296
+ end
297
+
298
+ raise_if_slot_ends_with_question_mark(slot_name)
299
+ raise_if_slot_registered(slot_name)
110
300
  end
111
301
 
112
- slot = slots[slot_name]
302
+ def validate_singular_slot_name(slot_name)
303
+ if slot_name.to_sym == :content
304
+ raise ContentSlotNameError.new(name)
305
+ end
306
+
307
+ if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
308
+ raise ReservedSingularSlotNameError.new(name, slot_name)
309
+ end
113
310
 
114
- # The class name of the Slot, such as Header
115
- slot_class = self.class.const_get(slot[:class_name])
311
+ raise_if_slot_ends_with_question_mark(slot_name)
312
+ raise_if_slot_registered(slot_name)
313
+ end
116
314
 
117
- unless slot_class <= ViewComponent::Slot
118
- raise ArgumentError.new "#{slot[:class_name]} must inherit from ViewComponent::Slot"
315
+ def raise_if_slot_registered(slot_name)
316
+ if registered_slots.key?(slot_name)
317
+ # TODO remove? This breaks overriding slots when slots are inherited
318
+ raise RedefinedSlotError.new(name, slot_name)
319
+ end
119
320
  end
120
321
 
121
- # Instantiate Slot class, accommodating Slots that don't accept arguments
122
- slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
322
+ def raise_if_slot_ends_with_question_mark(slot_name)
323
+ raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.ends_with?("?")
324
+ end
325
+ end
123
326
 
124
- # Capture block and assign to slot_instance#content
125
- slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block
327
+ def get_slot(slot_name)
328
+ content unless content_evaluated? # ensure content is loaded so slots will be defined
329
+
330
+ slot = self.class.registered_slots[slot_name]
331
+ @__vc_set_slots ||= {}
332
+
333
+ if @__vc_set_slots[slot_name]
334
+ return @__vc_set_slots[slot_name]
335
+ end
126
336
 
127
337
  if slot[:collection]
128
- # Initialize instance variable as an empty array
129
- # if slot is a collection and has yet to be initialized
130
- unless instance_variable_defined?(slot[:instance_variable_name])
131
- instance_variable_set(slot[:instance_variable_name], [])
338
+ []
339
+ end
340
+ end
341
+
342
+ def set_slot(slot_name, slot_definition = nil, *args, &block)
343
+ slot_definition ||= self.class.registered_slots[slot_name]
344
+ slot = Slot.new(self)
345
+
346
+ # Passing the block to the sub-component wrapper like this has two
347
+ # benefits:
348
+ #
349
+ # 1. If this is a `content_area` style sub-component, we will render the
350
+ # block via the `slot`
351
+ #
352
+ # 2. Since we've to pass block content to components when calling
353
+ # `render`, evaluating the block here would require us to call
354
+ # `view_context.capture` twice, which is slower
355
+ slot.__vc_content_block = block if block
356
+
357
+ # If class
358
+ if slot_definition[:renderable]
359
+ slot.__vc_component_instance = slot_definition[:renderable].new(*args)
360
+ # If class name as a string
361
+ elsif slot_definition[:renderable_class_name]
362
+ slot.__vc_component_instance =
363
+ self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
364
+ # If passed a lambda
365
+ elsif slot_definition[:renderable_function]
366
+ # Use `bind(self)` to ensure lambda is executed in the context of the
367
+ # current component. This is necessary to allow the lambda to access helper
368
+ # methods like `content_tag` as well as parent component state.
369
+ renderable_function = slot_definition[:renderable_function].bind(self)
370
+ renderable_value =
371
+ if block
372
+ renderable_function.call(*args) do |*rargs|
373
+ view_context.capture(*rargs, &block)
374
+ end
375
+ else
376
+ renderable_function.call(*args)
377
+ end
378
+
379
+ # Function calls can return components, so if it's a component handle it specially
380
+ if renderable_value.respond_to?(:render_in)
381
+ slot.__vc_component_instance = renderable_value
382
+ else
383
+ slot.__vc_content = renderable_value
132
384
  end
385
+ end
133
386
 
134
- # Append Slot instance to collection accessor Array
135
- instance_variable_get(slot[:instance_variable_name]) << slot_instance
387
+ @__vc_set_slots ||= {}
388
+
389
+ if slot_definition[:collection]
390
+ @__vc_set_slots[slot_name] ||= []
391
+ @__vc_set_slots[slot_name].push(slot)
136
392
  else
137
- # Assign the Slot instance to the slot accessor
138
- instance_variable_set(slot[:instance_variable_name], slot_instance)
393
+ @__vc_set_slots[slot_name] = slot
139
394
  end
140
395
 
141
- # Return nil, as this method shouldn't output anything to the view itself.
142
- nil
396
+ slot
397
+ end
398
+ ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
399
+
400
+ def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
401
+ slot_definition = self.class.registered_slots[slot_name]
402
+
403
+ if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name])
404
+ raise ContentAlreadySetForPolymorphicSlotError.new(slot_name)
405
+ end
406
+
407
+ poly_def = slot_definition[:renderable_hash][poly_type]
408
+
409
+ set_slot(slot_name, poly_def, *args, &block)
143
410
  end
411
+ ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
144
412
  end
145
413
  end
@@ -10,12 +10,12 @@ module ViewComponent
10
10
  # @param layout [String] The (optional) layout to use.
11
11
  # @return [Proc] A block that can be used to visit the path of the inline rendered component.
12
12
  def with_rendered_component_path(fragment, layout: false, &block)
13
- # Add './tmp/view_components/' directory if it doesn't exist to store the rendered component HTML
14
- FileUtils.mkdir_p("./tmp/view_components/") unless Dir.exist?("./tmp/view_components/")
15
-
16
- file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
13
+ file = Tempfile.new(
14
+ ["rendered_#{fragment.class.name}", ".html"],
15
+ ViewComponentsSystemTestController.temp_dir
16
+ )
17
17
  begin
18
- file.write(controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
18
+ file.write(vc_test_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
19
19
  file.rewind
20
20
 
21
21
  block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")