view_component 2.82.0 → 3.1.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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/view_component/preview_actions.rb +4 -0
- data/app/controllers/view_components_system_test_controller.rb +24 -1
- data/app/helpers/preview_helper.rb +2 -4
- data/docs/CHANGELOG.md +320 -0
- data/lib/view_component/base.rb +50 -96
- data/lib/view_component/capture_compatibility.rb +44 -0
- data/lib/view_component/collection.rb +2 -5
- data/lib/view_component/compiler.rb +51 -28
- data/lib/view_component/config.rb +9 -13
- data/lib/view_component/deprecation.rb +1 -1
- data/lib/view_component/docs_builder_component.html.erb +5 -1
- data/lib/view_component/docs_builder_component.rb +28 -9
- data/lib/view_component/engine.rb +12 -22
- data/lib/view_component/errors.rb +223 -0
- data/lib/view_component/inline_template.rb +55 -0
- data/lib/view_component/preview.rb +1 -7
- data/lib/view_component/rails/tasks/view_component.rake +1 -1
- data/lib/view_component/slot.rb +109 -1
- data/lib/view_component/slotable.rb +364 -96
- data/lib/view_component/system_test_helpers.rb +5 -5
- data/lib/view_component/test_helpers.rb +67 -54
- data/lib/view_component/translatable.rb +35 -23
- data/lib/view_component/version.rb +4 -3
- data/lib/view_component/with_content_helper.rb +3 -8
- data/lib/view_component.rb +3 -12
- metadata +45 -34
- data/lib/view_component/content_areas.rb +0 -56
- data/lib/view_component/polymorphic_slots.rb +0 -103
- data/lib/view_component/preview_template_error.rb +0 -6
- data/lib/view_component/slot_v2.rb +0 -98
- data/lib/view_component/slotable_v2.rb +0 -391
- 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)
|
data/lib/view_component/slot.rb
CHANGED
@@ -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
|
-
|
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 :
|
14
|
-
self.
|
18
|
+
class_attribute :registered_slots
|
19
|
+
self.registered_slots = {}
|
15
20
|
end
|
16
21
|
|
17
22
|
class_methods do
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# )
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
#
|
37
|
-
|
38
|
-
|
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
|
-
|
42
|
-
accessor_name =
|
226
|
+
poly_slot_name ||=
|
43
227
|
if collection
|
44
|
-
#
|
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
|
-
|
233
|
+
memo[poly_type] = define_slot(
|
234
|
+
poly_slot_name, collection: collection, callable: poly_callable
|
235
|
+
)
|
53
236
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
#
|
72
|
-
|
248
|
+
define_method "with_#{poly_slot_name}_content" do |content|
|
249
|
+
send(setter_method_name) { content.to_s }
|
73
250
|
|
74
|
-
|
75
|
-
|
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
|
-
|
84
|
-
# Clone slot configuration into child class
|
85
|
-
# see #test_slots_pollution
|
86
|
-
child.slots = slots.clone
|
261
|
+
private
|
87
262
|
|
88
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
311
|
+
raise_if_slot_ends_with_question_mark(slot_name)
|
312
|
+
raise_if_slot_registered(slot_name)
|
313
|
+
end
|
116
314
|
|
117
|
-
|
118
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
125
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
138
|
-
instance_variable_set(slot[:instance_variable_name], slot_instance)
|
393
|
+
@__vc_set_slots[slot_name] = slot
|
139
394
|
end
|
140
395
|
|
141
|
-
|
142
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
file = Tempfile.new(
|
14
|
+
["rendered_#{fragment.class.name}", ".html"],
|
15
|
+
ViewComponentsSystemTestController.temp_dir
|
16
|
+
)
|
17
17
|
begin
|
18
|
-
file.write(
|
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}")
|