view_component 2.82.0 → 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,391 +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
-
21
- class_attribute :_warn_on_deprecated_slot_setter
22
- self._warn_on_deprecated_slot_setter = false
23
- end
24
-
25
- class_methods do
26
- ##
27
- # Enables deprecations coming to the Slots API in ViewComponent v3
28
- #
29
- def warn_on_deprecated_slot_setter
30
- self._warn_on_deprecated_slot_setter = true
31
- end
32
-
33
- ##
34
- # Registers a sub-component
35
- #
36
- # = Example
37
- #
38
- # renders_one :header -> (classes:) do
39
- # HeaderComponent.new(classes: classes)
40
- # end
41
- #
42
- # # OR
43
- #
44
- # renders_one :header, HeaderComponent
45
- #
46
- # where `HeaderComponent` is defined as:
47
- #
48
- # class HeaderComponent < ViewComponent::Base
49
- # def initialize(classes:)
50
- # @classes = classes
51
- # end
52
- # end
53
- #
54
- # and has the following template:
55
- #
56
- # <header class="<%= @classes %>">
57
- # <%= content %>
58
- # </header>
59
- #
60
- # = Rendering sub-component content
61
- #
62
- # The component's sidecar template can access the sub-component by calling a
63
- # helper method with the same name as the sub-component.
64
- #
65
- # <h1>
66
- # <%= header do %>
67
- # My header title
68
- # <% end %>
69
- # </h1>
70
- #
71
- # = Setting sub-component content
72
- #
73
- # Consumers of the component can render a sub-component by calling a
74
- # helper method with the same name as the slot prefixed with `with_`.
75
- #
76
- # <%= render_inline(MyComponent.new) do |component| %>
77
- # <% component.with_header(classes: "Foo") do %>
78
- # <p>Bar</p>
79
- # <% end %>
80
- # <% end %>
81
- def renders_one(slot_name, callable = nil)
82
- validate_singular_slot_name(slot_name)
83
- validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
84
-
85
- define_method :"with_#{slot_name}" do |*args, &block|
86
- set_slot(slot_name, nil, *args, &block)
87
- end
88
- ruby2_keywords(:"with_#{slot_name}") if respond_to?(:ruby2_keywords, true)
89
-
90
- define_method slot_name do |*args, &block|
91
- if args.empty? && block.nil?
92
- get_slot(slot_name)
93
- else
94
- if _warn_on_deprecated_slot_setter
95
- stack = caller_locations(3)
96
-
97
- ViewComponent::Deprecation.deprecation_warning(
98
- "Setting a slot with `##{slot_name}`",
99
- "use `#with_#{slot_name}` to set the slot instead",
100
- stack
101
- )
102
- end
103
-
104
- set_slot(slot_name, nil, *args, &block)
105
- end
106
- end
107
- ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
108
-
109
- define_method "#{slot_name}?" do
110
- get_slot(slot_name).present?
111
- end
112
-
113
- register_slot(slot_name, collection: false, callable: callable)
114
- end
115
-
116
- ##
117
- # Registers a collection sub-component
118
- #
119
- # = Example
120
- #
121
- # renders_many :items, -> (name:) { ItemComponent.new(name: name }
122
- #
123
- # # OR
124
- #
125
- # renders_many :items, ItemComponent
126
- #
127
- # = Rendering sub-components
128
- #
129
- # The component's sidecar template can access the slot by calling a
130
- # helper method with the same name as the slot.
131
- #
132
- # <h1>
133
- # <% items.each do |item| %>
134
- # <%= item %>
135
- # <% end %>
136
- # </h1>
137
- #
138
- # = Setting sub-component content
139
- #
140
- # Consumers of the component can set the content of a slot by calling a
141
- # helper method with the same name as the slot prefixed with `with_`. The
142
- # method can be called multiple times to append to the slot.
143
- #
144
- # <%= render_inline(MyComponent.new) do |component| %>
145
- # <% component.with_item(name: "Foo") do %>
146
- # <p>One</p>
147
- # <% end %>
148
- #
149
- # <% component.with_item(name: "Bar") do %>
150
- # <p>two</p>
151
- # <% end %>
152
- # <% end %>
153
- def renders_many(slot_name, callable = nil)
154
- singular_name = ActiveSupport::Inflector.singularize(slot_name)
155
- validate_plural_slot_name(slot_name)
156
- validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
157
-
158
- # Define setter for singular names
159
- # for example `renders_many :items` allows fetching all tabs with
160
- # `component.tabs` and setting a tab with `component.tab`
161
-
162
- define_method singular_name do |*args, &block|
163
- if _warn_on_deprecated_slot_setter
164
- ViewComponent::Deprecation.deprecation_warning(
165
- "Setting a slot with `##{singular_name}`",
166
- "use `#with_#{singular_name}` to set the slot instead"
167
- )
168
- end
169
-
170
- set_slot(slot_name, nil, *args, &block)
171
- end
172
- ruby2_keywords(singular_name.to_sym) if respond_to?(:ruby2_keywords, true)
173
-
174
- define_method :"with_#{singular_name}" do |*args, &block|
175
- set_slot(slot_name, nil, *args, &block)
176
- end
177
- ruby2_keywords(:"with_#{singular_name}") if respond_to?(:ruby2_keywords, true)
178
-
179
- define_method :"with_#{slot_name}" do |collection_args = nil, &block|
180
- collection_args.map do |args|
181
- set_slot(slot_name, nil, **args, &block)
182
- end
183
- end
184
-
185
- # Instantiates and and adds multiple slots forwarding the first
186
- # argument to each slot constructor
187
- define_method slot_name do |collection_args = nil, &block|
188
- if collection_args.nil? && block.nil?
189
- get_slot(slot_name)
190
- else
191
- if _warn_on_deprecated_slot_setter
192
- ViewComponent::Deprecation.deprecation_warning(
193
- "Setting a slot with `##{slot_name}`",
194
- "use `#with_#{slot_name}` to set the slot instead"
195
- )
196
- end
197
-
198
- collection_args.map do |args|
199
- set_slot(slot_name, nil, **args, &block)
200
- end
201
- end
202
- end
203
-
204
- define_method "#{slot_name}?" do
205
- get_slot(slot_name).present?
206
- end
207
-
208
- register_slot(slot_name, collection: true, callable: callable)
209
- end
210
-
211
- def slot_type(slot_name)
212
- registered_slot = registered_slots[slot_name]
213
- if registered_slot
214
- registered_slot[:collection] ? :collection : :single
215
- else
216
- plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym
217
- plural_registered_slot = registered_slots[plural_slot_name]
218
- plural_registered_slot&.fetch(:collection) ? :collection_item : nil
219
- end
220
- end
221
-
222
- # Clone slot configuration into child class
223
- # see #test_slots_pollution
224
- def inherited(child)
225
- child.registered_slots = registered_slots.clone
226
- super
227
- end
228
-
229
- private
230
-
231
- def register_slot(slot_name, **kwargs)
232
- registered_slots[slot_name] = define_slot(slot_name, **kwargs)
233
- end
234
-
235
- def define_slot(slot_name, collection:, callable:)
236
- # Setup basic slot data
237
- slot = {
238
- collection: collection
239
- }
240
- return slot unless callable
241
-
242
- # If callable responds to `render_in`, we set it on the slot as a renderable
243
- if callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
244
- slot[:renderable] = callable
245
- elsif callable.is_a?(String)
246
- # If callable is a string, we assume it's referencing an internal class
247
- slot[:renderable_class_name] = callable
248
- elsif callable.respond_to?(:call)
249
- # If slot doesn't respond to `render_in`, we assume it's a proc,
250
- # define a method, and save a reference to it to call when setting
251
- method_name = :"_call_#{slot_name}"
252
- define_method method_name, &callable
253
- slot[:renderable_function] = instance_method(method_name)
254
- else
255
- raise(
256
- ArgumentError,
257
- "invalid slot definition. Please pass a class, string, or callable (i.e. proc, lambda, etc)"
258
- )
259
- end
260
-
261
- slot
262
- end
263
-
264
- def validate_plural_slot_name(slot_name)
265
- if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
266
- raise ArgumentError.new(
267
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
268
- "To fix this issue, choose a different name."
269
- )
270
- end
271
-
272
- raise_if_slot_ends_with_question_mark(slot_name)
273
- raise_if_slot_registered(slot_name)
274
- end
275
-
276
- def validate_singular_slot_name(slot_name)
277
- if slot_name.to_sym == :content
278
- raise ArgumentError.new(
279
- "#{self} declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
280
- "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" \
281
- "To fix this issue, either use the `content` accessor directly or choose a different slot name."
282
- )
283
- end
284
-
285
- if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
286
- raise ArgumentError.new(
287
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
288
- "To fix this issue, choose a different name."
289
- )
290
- end
291
-
292
- raise_if_slot_ends_with_question_mark(slot_name)
293
- raise_if_slot_registered(slot_name)
294
- end
295
-
296
- def raise_if_slot_registered(slot_name)
297
- if registered_slots.key?(slot_name)
298
- # TODO remove? This breaks overriding slots when slots are inherited
299
- raise ArgumentError.new(
300
- "#{self} declares the #{slot_name} slot multiple times.\n\n" \
301
- "To fix this issue, choose a different slot name."
302
- )
303
- end
304
- end
305
-
306
- def raise_if_slot_ends_with_question_mark(slot_name)
307
- if slot_name.to_s.ends_with?("?")
308
- raise ArgumentError.new(
309
- "#{self} declares a slot named #{slot_name}, which ends with a question mark.\n\n" \
310
- "This is not allowed because the ViewComponent framework already provides predicate " \
311
- "methods ending in `?`.\n\n" \
312
- "To fix this issue, choose a different name."
313
- )
314
- end
315
- end
316
- end
317
-
318
- def get_slot(slot_name)
319
- content unless content_evaluated? # ensure content is loaded so slots will be defined
320
-
321
- slot = self.class.registered_slots[slot_name]
322
- @__vc_set_slots ||= {}
323
-
324
- if @__vc_set_slots[slot_name]
325
- return @__vc_set_slots[slot_name]
326
- end
327
-
328
- if slot[:collection]
329
- []
330
- end
331
- end
332
-
333
- def set_slot(slot_name, slot_definition = nil, *args, &block)
334
- slot_definition ||= self.class.registered_slots[slot_name]
335
- slot = SlotV2.new(self)
336
-
337
- # Passing the block to the sub-component wrapper like this has two
338
- # benefits:
339
- #
340
- # 1. If this is a `content_area` style sub-component, we will render the
341
- # block via the `slot`
342
- #
343
- # 2. Since we've to pass block content to components when calling
344
- # `render`, evaluating the block here would require us to call
345
- # `view_context.capture` twice, which is slower
346
- slot.__vc_content_block = block if block
347
-
348
- # If class
349
- if slot_definition[:renderable]
350
- slot.__vc_component_instance = slot_definition[:renderable].new(*args)
351
- # If class name as a string
352
- elsif slot_definition[:renderable_class_name]
353
- slot.__vc_component_instance =
354
- self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
355
- # If passed a lambda
356
- elsif slot_definition[:renderable_function]
357
- # Use `bind(self)` to ensure lambda is executed in the context of the
358
- # current component. This is necessary to allow the lambda to access helper
359
- # methods like `content_tag` as well as parent component state.
360
- renderable_function = slot_definition[:renderable_function].bind(self)
361
- renderable_value =
362
- if block
363
- renderable_function.call(*args) do |*rargs|
364
- view_context.capture(*rargs, &block)
365
- end
366
- else
367
- renderable_function.call(*args)
368
- end
369
-
370
- # Function calls can return components, so if it's a component handle it specially
371
- if renderable_value.respond_to?(:render_in)
372
- slot.__vc_component_instance = renderable_value
373
- else
374
- slot.__vc_content = renderable_value
375
- end
376
- end
377
-
378
- @__vc_set_slots ||= {}
379
-
380
- if slot_definition[:collection]
381
- @__vc_set_slots[slot_name] ||= []
382
- @__vc_set_slots[slot_name].push(slot)
383
- else
384
- @__vc_set_slots[slot_name] = slot
385
- end
386
-
387
- slot
388
- end
389
- ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
390
- end
391
- end