view_component 3.0.0.rc1 → 3.0.0.rc2

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.

@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ViewComponent
4
- module PolymorphicSlots
5
- # In older rails versions, using a concern isn't a good idea here because they appear to not work with
6
- # Module#prepend and class methods.
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
-
19
- base.singleton_class.prepend(ClassMethods)
20
- base.include(InstanceMethods)
21
- end
22
-
23
- module ClassMethods
24
- def renders_one(slot_name, callable = nil)
25
- return super unless callable.is_a?(Hash) && callable.key?(:types)
26
-
27
- validate_singular_slot_name(slot_name)
28
- register_polymorphic_slot(slot_name, callable[:types], collection: false)
29
- end
30
-
31
- def renders_many(slot_name, callable = nil)
32
- return super unless callable.is_a?(Hash) && callable.key?(:types)
33
-
34
- validate_plural_slot_name(slot_name)
35
- register_polymorphic_slot(slot_name, callable[:types], collection: true)
36
- end
37
-
38
- def register_polymorphic_slot(slot_name, types, collection:)
39
- unless types.empty?
40
- getter_name = slot_name
41
-
42
- define_method(getter_name) do
43
- get_slot(slot_name)
44
- end
45
-
46
- define_method("#{getter_name}?") do
47
- get_slot(slot_name).present?
48
- end
49
- end
50
-
51
- renderable_hash = types.each_with_object({}) do |(poly_type, poly_callable), memo|
52
- memo[poly_type] = define_slot(
53
- "#{slot_name}_#{poly_type}", collection: collection, callable: poly_callable
54
- )
55
-
56
- setter_name =
57
- if collection
58
- "#{ActiveSupport::Inflector.singularize(slot_name)}_#{poly_type}"
59
- else
60
- "#{slot_name}_#{poly_type}"
61
- end
62
-
63
- define_method("with_#{setter_name}") do |*args, &block|
64
- set_polymorphic_slot(slot_name, poly_type, *args, &block)
65
- end
66
- ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
67
- end
68
-
69
- registered_slots[slot_name] = {
70
- collection: collection,
71
- renderable_hash: renderable_hash
72
- }
73
- end
74
- end
75
-
76
- module InstanceMethods
77
- def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
78
- slot_definition = self.class.registered_slots[slot_name]
79
-
80
- if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name])
81
- raise ArgumentError, "content for slot '#{slot_name}' has already been provided"
82
- end
83
-
84
- poly_def = slot_definition[:renderable_hash][poly_type]
85
-
86
- set_slot(slot_name, poly_def, *args, &block)
87
- end
88
- ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
89
- end
90
- end
91
- end
@@ -1,336 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/concern"
4
- require "view_component/slot_v2"
5
-
6
- module ViewComponent
7
- module SlotableV2
8
- extend ActiveSupport::Concern
9
-
10
- RESERVED_NAMES = {
11
- singular: %i[content render].freeze,
12
- plural: %i[contents renders].freeze
13
- }.freeze
14
-
15
- # Setup component slot state
16
- included do
17
- # Hash of registered Slots
18
- class_attribute :registered_slots
19
- self.registered_slots = {}
20
- end
21
-
22
- class_methods do
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
- def renders_one(slot_name, callable = nil)
72
- validate_singular_slot_name(slot_name)
73
- validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
74
-
75
- define_method :"with_#{slot_name}" do |*args, &block|
76
- set_slot(slot_name, nil, *args, &block)
77
- end
78
- ruby2_keywords(:"with_#{slot_name}") if respond_to?(:ruby2_keywords, true)
79
-
80
- define_method slot_name do |*args, &block|
81
- get_slot(slot_name)
82
- end
83
- ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
84
-
85
- define_method "#{slot_name}?" do
86
- get_slot(slot_name).present?
87
- end
88
-
89
- register_slot(slot_name, collection: false, callable: callable)
90
- end
91
-
92
- ##
93
- # Registers a collection sub-component
94
- #
95
- # = Example
96
- #
97
- # renders_many :items, -> (name:) { ItemComponent.new(name: name }
98
- #
99
- # # OR
100
- #
101
- # renders_many :items, ItemComponent
102
- #
103
- # = Rendering sub-components
104
- #
105
- # The component's sidecar template can access the slot by calling a
106
- # helper method with the same name as the slot.
107
- #
108
- # <h1>
109
- # <% items.each do |item| %>
110
- # <%= item %>
111
- # <% end %>
112
- # </h1>
113
- #
114
- # = Setting sub-component content
115
- #
116
- # Consumers of the component can set the content of a slot by calling a
117
- # helper method with the same name as the slot prefixed with `with_`. The
118
- # method can be called multiple times to append to the slot.
119
- #
120
- # <%= render_inline(MyComponent.new) do |component| %>
121
- # <% component.with_item(name: "Foo") do %>
122
- # <p>One</p>
123
- # <% end %>
124
- #
125
- # <% component.with_item(name: "Bar") do %>
126
- # <p>two</p>
127
- # <% end %>
128
- # <% end %>
129
- def renders_many(slot_name, callable = nil)
130
- singular_name = ActiveSupport::Inflector.singularize(slot_name)
131
- validate_plural_slot_name(slot_name)
132
- validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
133
-
134
- define_method :"with_#{singular_name}" do |*args, &block|
135
- set_slot(slot_name, nil, *args, &block)
136
- end
137
- ruby2_keywords(:"with_#{singular_name}") if respond_to?(:ruby2_keywords, true)
138
-
139
- define_method :"with_#{slot_name}" do |collection_args = nil, &block|
140
- collection_args.map do |args|
141
- set_slot(slot_name, nil, **args, &block)
142
- end
143
- end
144
-
145
- define_method slot_name do |collection_args = nil, &block|
146
- get_slot(slot_name)
147
- end
148
-
149
- define_method "#{slot_name}?" do
150
- get_slot(slot_name).present?
151
- end
152
-
153
- register_slot(slot_name, collection: true, callable: callable)
154
- end
155
-
156
- def slot_type(slot_name)
157
- registered_slot = registered_slots[slot_name]
158
- if registered_slot
159
- registered_slot[:collection] ? :collection : :single
160
- else
161
- plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym
162
- plural_registered_slot = registered_slots[plural_slot_name]
163
- plural_registered_slot&.fetch(:collection) ? :collection_item : nil
164
- end
165
- end
166
-
167
- # Clone slot configuration into child class
168
- # see #test_slots_pollution
169
- def inherited(child)
170
- child.registered_slots = registered_slots.clone
171
- super
172
- end
173
-
174
- private
175
-
176
- def register_slot(slot_name, **kwargs)
177
- registered_slots[slot_name] = define_slot(slot_name, **kwargs)
178
- end
179
-
180
- def define_slot(slot_name, collection:, callable:)
181
- # Setup basic slot data
182
- slot = {
183
- collection: collection
184
- }
185
- return slot unless callable
186
-
187
- # If callable responds to `render_in`, we set it on the slot as a renderable
188
- if callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
189
- slot[:renderable] = callable
190
- elsif callable.is_a?(String)
191
- # If callable is a string, we assume it's referencing an internal class
192
- slot[:renderable_class_name] = callable
193
- elsif callable.respond_to?(:call)
194
- # If slot doesn't respond to `render_in`, we assume it's a proc,
195
- # define a method, and save a reference to it to call when setting
196
- method_name = :"_call_#{slot_name}"
197
- define_method method_name, &callable
198
- slot[:renderable_function] = instance_method(method_name)
199
- else
200
- raise(
201
- ArgumentError,
202
- "invalid slot definition. Please pass a class, string, or callable (i.e. proc, lambda, etc)"
203
- )
204
- end
205
-
206
- slot
207
- end
208
-
209
- def validate_plural_slot_name(slot_name)
210
- if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
211
- raise ArgumentError.new(
212
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
213
- "To fix this issue, choose a different name."
214
- )
215
- end
216
-
217
- raise_if_slot_ends_with_question_mark(slot_name)
218
- raise_if_slot_registered(slot_name)
219
- end
220
-
221
- def validate_singular_slot_name(slot_name)
222
- if slot_name.to_sym == :content
223
- raise ArgumentError.new(
224
- "#{self} declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
225
- "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" \
226
- "To fix this issue, either use the `content` accessor directly or choose a different slot name."
227
- )
228
- end
229
-
230
- if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
231
- raise ArgumentError.new(
232
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
233
- "To fix this issue, choose a different name."
234
- )
235
- end
236
-
237
- raise_if_slot_ends_with_question_mark(slot_name)
238
- raise_if_slot_registered(slot_name)
239
- end
240
-
241
- def raise_if_slot_registered(slot_name)
242
- if registered_slots.key?(slot_name)
243
- # TODO remove? This breaks overriding slots when slots are inherited
244
- raise ArgumentError.new(
245
- "#{self} declares the #{slot_name} slot multiple times.\n\n" \
246
- "To fix this issue, choose a different slot name."
247
- )
248
- end
249
- end
250
-
251
- def raise_if_slot_ends_with_question_mark(slot_name)
252
- if slot_name.to_s.ends_with?("?")
253
- raise ArgumentError.new(
254
- "#{self} declares a slot named #{slot_name}, which ends with a question mark.\n\n" \
255
- "This is not allowed because the ViewComponent framework already provides predicate " \
256
- "methods ending in `?`.\n\n" \
257
- "To fix this issue, choose a different name."
258
- )
259
- end
260
- end
261
- end
262
-
263
- def get_slot(slot_name)
264
- content unless content_evaluated? # ensure content is loaded so slots will be defined
265
-
266
- slot = self.class.registered_slots[slot_name]
267
- @__vc_set_slots ||= {}
268
-
269
- if @__vc_set_slots[slot_name]
270
- return @__vc_set_slots[slot_name]
271
- end
272
-
273
- if slot[:collection]
274
- []
275
- end
276
- end
277
-
278
- def set_slot(slot_name, slot_definition = nil, *args, &block)
279
- slot_definition ||= self.class.registered_slots[slot_name]
280
- slot = SlotV2.new(self)
281
-
282
- # Passing the block to the sub-component wrapper like this has two
283
- # benefits:
284
- #
285
- # 1. If this is a `content_area` style sub-component, we will render the
286
- # block via the `slot`
287
- #
288
- # 2. Since we've to pass block content to components when calling
289
- # `render`, evaluating the block here would require us to call
290
- # `view_context.capture` twice, which is slower
291
- slot.__vc_content_block = block if block
292
-
293
- # If class
294
- if slot_definition[:renderable]
295
- slot.__vc_component_instance = slot_definition[:renderable].new(*args)
296
- # If class name as a string
297
- elsif slot_definition[:renderable_class_name]
298
- slot.__vc_component_instance =
299
- self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
300
- # If passed a lambda
301
- elsif slot_definition[:renderable_function]
302
- # Use `bind(self)` to ensure lambda is executed in the context of the
303
- # current component. This is necessary to allow the lambda to access helper
304
- # methods like `content_tag` as well as parent component state.
305
- renderable_function = slot_definition[:renderable_function].bind(self)
306
- renderable_value =
307
- if block
308
- renderable_function.call(*args) do |*rargs|
309
- view_context.capture(*rargs, &block)
310
- end
311
- else
312
- renderable_function.call(*args)
313
- end
314
-
315
- # Function calls can return components, so if it's a component handle it specially
316
- if renderable_value.respond_to?(:render_in)
317
- slot.__vc_component_instance = renderable_value
318
- else
319
- slot.__vc_content = renderable_value
320
- end
321
- end
322
-
323
- @__vc_set_slots ||= {}
324
-
325
- if slot_definition[:collection]
326
- @__vc_set_slots[slot_name] ||= []
327
- @__vc_set_slots[slot_name].push(slot)
328
- else
329
- @__vc_set_slots[slot_name] = slot
330
- end
331
-
332
- slot
333
- end
334
- ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
335
- end
336
- end