view_component 2.22.1 → 2.25.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/CHANGELOG.md +53 -1
- data/lib/rails/generators/component/component_generator.rb +7 -0
- data/lib/rails/generators/component/templates/component.rb.tt +8 -0
- data/lib/rails/generators/erb/component_generator.rb +7 -4
- data/lib/rails/generators/preview/component_generator.rb +21 -0
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +5 -0
- data/lib/view_component/base.rb +33 -16
- data/lib/view_component/collection.rb +8 -5
- data/lib/view_component/compiler.rb +24 -9
- data/lib/view_component/slot_v2.rb +65 -0
- data/lib/view_component/slotable.rb +14 -0
- data/lib/view_component/slotable_v2.rb +249 -0
- data/lib/view_component/version.rb +2 -2
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed1ee878b4b1519adcb75b06689a3341367cca5aa8ae4dd087cb2fba0c06a355
|
4
|
+
data.tar.gz: 70459f53b555b8edf6675346a42c1be87bb36267bce23ffb9cc4a639731c1b77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ac2189345c278608e8dc96ec1c7f90472991aefc86da43968489f3ea3cff4c9306ef8280553b2dad6a7a35b24e74eabd2269b3efcba4026b00b642dcac0dcf4
|
7
|
+
data.tar.gz: 6a393762db37291a6f6eaa2e19575aa035f103cc6c0fe71c96648fb827ed0e140c0e9d9f297901f699f7abd3e4946ca8e048bf53b1dbe62e47663e2bb591918e
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,58 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
-
##
|
3
|
+
## main
|
4
|
+
|
5
|
+
## 2.25.0
|
6
|
+
|
7
|
+
* Add `--preview` generator option to create an associated preview file.
|
8
|
+
|
9
|
+
*Bob Maerten*
|
10
|
+
|
11
|
+
* Add argument validation to avoid `content` override.
|
12
|
+
|
13
|
+
*Manuel Puyol*
|
14
|
+
|
15
|
+
## 2.24.0
|
16
|
+
|
17
|
+
* Add `--inline` option to the erb generator. Prevents default erb template from being created and creates a component with a call method.
|
18
|
+
|
19
|
+
*Nachiket Pusalkar*
|
20
|
+
|
21
|
+
* Add test case for checking presence of `content` in `#render?`.
|
22
|
+
|
23
|
+
*Joel Hawksley*
|
24
|
+
|
25
|
+
* Rename `master` branch to `main`.
|
26
|
+
|
27
|
+
*Joel Hawksley*
|
28
|
+
|
29
|
+
## 2.23.2
|
30
|
+
|
31
|
+
* Fix bug where rendering a component `with_collection` from a controller raised an error.
|
32
|
+
|
33
|
+
*Joel Hawksley*
|
34
|
+
|
35
|
+
## 2.23.1
|
36
|
+
|
37
|
+
* Fixed out-of-order rendering bug in `ActionView::SlotableV2`
|
38
|
+
|
39
|
+
*Blake Williams*
|
40
|
+
|
41
|
+
## 2.23.0
|
42
|
+
|
43
|
+
* Add `ActionView::SlotableV2`
|
44
|
+
* `with_slot` becomes `renders_one`.
|
45
|
+
* `with_slot collection: true` becomes `renders_many`.
|
46
|
+
* Slot definitions now accept either a component class, component class name, or a lambda instead of a `class_name:` keyword argument.
|
47
|
+
* Slots now support positional arguments.
|
48
|
+
* Slots no longer use the `content` attribute to render content, instead relying on `to_s`. e.g. `<%= my_slot %>`.
|
49
|
+
* Slot values are no longer set via the `slot` method, and instead use the name of the slot.
|
50
|
+
|
51
|
+
*Blake Williams*
|
52
|
+
|
53
|
+
* Add `frozen_string_literal: true` to generated component template.
|
54
|
+
|
55
|
+
*Max Beizer*
|
4
56
|
|
5
57
|
## 2.22.1
|
6
58
|
|
@@ -7,6 +7,7 @@ module Rails
|
|
7
7
|
|
8
8
|
argument :attributes, type: :array, default: [], banner: "attribute"
|
9
9
|
check_class_collision suffix: "Component"
|
10
|
+
class_option :inline, type: :boolean, default: false
|
10
11
|
|
11
12
|
def create_component_file
|
12
13
|
template "component.rb", File.join("app/components", class_path, "#{file_name}_component.rb")
|
@@ -14,6 +15,8 @@ module Rails
|
|
14
15
|
|
15
16
|
hook_for :test_framework
|
16
17
|
|
18
|
+
hook_for :preview, type: :boolean
|
19
|
+
|
17
20
|
hook_for :template_engine do |instance, template_engine|
|
18
21
|
instance.invoke template_engine, [instance.name]
|
19
22
|
end
|
@@ -37,6 +40,10 @@ module Rails
|
|
37
40
|
def initialize_body
|
38
41
|
attributes.map { |attr| "@#{attr.name} = #{attr.name}" }.join("\n ")
|
39
42
|
end
|
43
|
+
|
44
|
+
def initialize_call_method_for_inline?
|
45
|
+
options["inline"]
|
46
|
+
end
|
40
47
|
end
|
41
48
|
end
|
42
49
|
end
|
@@ -1,7 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class <%= class_name %>Component < <%= parent_class %>
|
2
4
|
<%- if initialize_signature -%>
|
3
5
|
def initialize(<%= initialize_signature %>)
|
4
6
|
<%= initialize_body %>
|
5
7
|
end
|
6
8
|
<%- end -%>
|
9
|
+
<%- if initialize_call_method_for_inline? -%>
|
10
|
+
def call
|
11
|
+
content_tag :h1, "Hello world!"
|
12
|
+
end
|
13
|
+
<%- end -%>
|
14
|
+
|
7
15
|
end
|
@@ -7,6 +7,7 @@ module Erb
|
|
7
7
|
class ComponentGenerator < Base
|
8
8
|
source_root File.expand_path("templates", __dir__)
|
9
9
|
class_option :sidecar, type: :boolean, default: false
|
10
|
+
class_option :inline, type: :boolean, default: false
|
10
11
|
|
11
12
|
def copy_view_file
|
12
13
|
template "component.html.erb", destination
|
@@ -15,10 +16,12 @@ module Erb
|
|
15
16
|
private
|
16
17
|
|
17
18
|
def destination
|
18
|
-
if options["
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
if !options["inline"]
|
20
|
+
if options["sidecar"]
|
21
|
+
File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.erb")
|
22
|
+
else
|
23
|
+
File.join("app/components", class_path, "#{file_name}_component.html.erb")
|
24
|
+
end
|
22
25
|
end
|
23
26
|
end
|
24
27
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Preview
|
4
|
+
module Generators
|
5
|
+
class ComponentGenerator < ::Rails::Generators::NamedBase
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
check_class_collision suffix: "ComponentPreview"
|
9
|
+
|
10
|
+
def create_preview_file
|
11
|
+
template "component_preview.rb", File.join("test/components/previews", class_path, "#{file_name}_component_preview.rb")
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def file_name
|
17
|
+
@_file_name ||= super.sub(/_component\z/i, "")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/view_component/base.rb
CHANGED
@@ -6,6 +6,7 @@ require "view_component/collection"
|
|
6
6
|
require "view_component/compile_cache"
|
7
7
|
require "view_component/previewable"
|
8
8
|
require "view_component/slotable"
|
9
|
+
require "view_component/slotable_v2"
|
9
10
|
|
10
11
|
module ViewComponent
|
11
12
|
class Base < ActionView::Base
|
@@ -14,16 +15,14 @@ module ViewComponent
|
|
14
15
|
|
15
16
|
ViewContextCalledBeforeRenderError = Class.new(StandardError)
|
16
17
|
|
18
|
+
RESERVED_PARAMETER = :content
|
19
|
+
|
17
20
|
# For CSRF authenticity tokens in forms
|
18
21
|
delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
|
19
22
|
|
20
23
|
class_attribute :content_areas
|
21
24
|
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
|
22
25
|
|
23
|
-
# Hash of registered Slots
|
24
|
-
class_attribute :slots
|
25
|
-
self.slots = {}
|
26
|
-
|
27
26
|
# Entrypoint for rendering components.
|
28
27
|
#
|
29
28
|
# view_context: ActionView context from calling view
|
@@ -99,13 +98,16 @@ module ViewComponent
|
|
99
98
|
|
100
99
|
def initialize(*); end
|
101
100
|
|
102
|
-
#
|
103
|
-
#
|
101
|
+
# Re-use original view_context if we're not rendering a component.
|
102
|
+
#
|
103
|
+
# This prevents an exception when rendering a partial inside of a component that has also been rendered outside
|
104
|
+
# of the component. This is due to the partials compiled template method existing in the parent `view_context`,
|
105
|
+
# and not the component's `view_context`.
|
104
106
|
def render(options = {}, args = {}, &block)
|
105
|
-
if options.is_a?
|
106
|
-
view_context.render(options, args, &block)
|
107
|
-
else
|
107
|
+
if options.is_a? ViewComponent::Base
|
108
108
|
super
|
109
|
+
else
|
110
|
+
view_context.render(options, args, &block)
|
109
111
|
end
|
110
112
|
end
|
111
113
|
|
@@ -132,7 +134,10 @@ module ViewComponent
|
|
132
134
|
|
133
135
|
# For caching, such as #cache_if
|
134
136
|
def format
|
135
|
-
|
137
|
+
# Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
|
138
|
+
if defined?(@variant)
|
139
|
+
@variant
|
140
|
+
end
|
136
141
|
end
|
137
142
|
|
138
143
|
# Assign the provided content to the content area accessor
|
@@ -207,10 +212,6 @@ module ViewComponent
|
|
207
212
|
# Removes the first part of the path and the extension.
|
208
213
|
child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
|
209
214
|
|
210
|
-
# Clone slot configuration into child class
|
211
|
-
# see #test_slots_pollution
|
212
|
-
child.slots = self.slots.clone
|
213
|
-
|
214
215
|
super
|
215
216
|
end
|
216
217
|
|
@@ -265,7 +266,7 @@ module ViewComponent
|
|
265
266
|
parameter = validate_default ? collection_parameter : provided_collection_parameter
|
266
267
|
|
267
268
|
return unless parameter
|
268
|
-
return if
|
269
|
+
return if initialize_parameter_names.include?(parameter)
|
269
270
|
|
270
271
|
# If Ruby cannot parse the component class, then the initalize
|
271
272
|
# parameters will be empty and ViewComponent will not be able to render
|
@@ -282,8 +283,25 @@ module ViewComponent
|
|
282
283
|
)
|
283
284
|
end
|
284
285
|
|
286
|
+
# Ensure the component initializer does not define
|
287
|
+
# invalid parameters that could override the framework's
|
288
|
+
# methods.
|
289
|
+
def validate_initialization_parameters!
|
290
|
+
return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
|
291
|
+
|
292
|
+
raise ArgumentError.new(
|
293
|
+
"#{self} initializer cannot contain " \
|
294
|
+
"`#{RESERVED_PARAMETER}` since it will override a " \
|
295
|
+
"public ViewComponent method."
|
296
|
+
)
|
297
|
+
end
|
298
|
+
|
285
299
|
private
|
286
300
|
|
301
|
+
def initialize_parameter_names
|
302
|
+
initialize_parameters.map(&:last)
|
303
|
+
end
|
304
|
+
|
287
305
|
def initialize_parameters
|
288
306
|
instance_method(:initialize).parameters
|
289
307
|
end
|
@@ -291,7 +309,6 @@ module ViewComponent
|
|
291
309
|
def provided_collection_parameter
|
292
310
|
@provided_collection_parameter ||= nil
|
293
311
|
end
|
294
|
-
|
295
312
|
end
|
296
313
|
|
297
314
|
ActiveSupport.run_load_hooks(:view_component, self)
|
@@ -4,14 +4,17 @@ require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1
|
|
4
4
|
|
5
5
|
module ViewComponent
|
6
6
|
class Collection
|
7
|
+
attr_reader :component
|
8
|
+
delegate :format, to: :component
|
9
|
+
|
7
10
|
def render_in(view_context, &block)
|
8
11
|
iterator = ActionView::PartialIteration.new(@collection.size)
|
9
12
|
|
10
|
-
|
11
|
-
|
13
|
+
component.compile(raise_errors: true)
|
14
|
+
component.validate_collection_parameter!(validate_default: true)
|
12
15
|
|
13
16
|
@collection.map do |item|
|
14
|
-
content =
|
17
|
+
content = component.new(**component_options(item, iterator)).render_in(view_context, &block)
|
15
18
|
iterator.iterate!
|
16
19
|
content
|
17
20
|
end.join.html_safe
|
@@ -34,8 +37,8 @@ module ViewComponent
|
|
34
37
|
end
|
35
38
|
|
36
39
|
def component_options(item, iterator)
|
37
|
-
item_options = {
|
38
|
-
item_options[
|
40
|
+
item_options = { component.collection_parameter => item }
|
41
|
+
item_options[component.collection_counter_parameter] = iterator.index + 1 if component.counter_argument_present?
|
39
42
|
|
40
43
|
@options.merge(item_options)
|
41
44
|
end
|
@@ -46,7 +46,10 @@ module ViewComponent
|
|
46
46
|
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
|
47
47
|
end
|
48
48
|
|
49
|
-
|
49
|
+
if raise_errors
|
50
|
+
component_class.validate_initialization_parameters!
|
51
|
+
component_class.validate_collection_parameter!
|
52
|
+
end
|
50
53
|
|
51
54
|
templates.each do |template|
|
52
55
|
# Remove existing compiled template methods,
|
@@ -146,21 +149,35 @@ module ViewComponent
|
|
146
149
|
source_location = component_class.source_location
|
147
150
|
return [] unless source_location
|
148
151
|
|
149
|
-
location_without_extension = source_location.chomp(File.extname(source_location))
|
150
|
-
|
151
152
|
extensions = ActionView::Template.template_handler_extensions.join(",")
|
152
153
|
|
153
|
-
# view files in the same directory as the component
|
154
|
-
sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
|
155
|
-
|
156
154
|
# view files in a directory named like the component
|
157
155
|
directory = File.dirname(source_location)
|
158
156
|
filename = File.basename(source_location, ".rb")
|
159
157
|
component_name = component_class.name.demodulize.underscore
|
160
158
|
|
159
|
+
# Add support for nested components defined in the same file.
|
160
|
+
#
|
161
|
+
# e.g.
|
162
|
+
#
|
163
|
+
# class MyComponent < ViewComponent::Base
|
164
|
+
# class MyOtherComponent < ViewComponent::Base
|
165
|
+
# end
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
|
169
|
+
nested_component_files = if component_class.name.include?("::") && component_name != filename
|
170
|
+
Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
|
171
|
+
else
|
172
|
+
[]
|
173
|
+
end
|
174
|
+
|
175
|
+
# view files in the same directory as the component
|
176
|
+
sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
|
177
|
+
|
161
178
|
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
|
162
179
|
|
163
|
-
(sidecar_files - [source_location] + sidecar_directory_files)
|
180
|
+
(sidecar_files - [source_location] + sidecar_directory_files + nested_component_files)
|
164
181
|
end
|
165
182
|
|
166
183
|
def inline_calls
|
@@ -190,7 +207,6 @@ module ViewComponent
|
|
190
207
|
end
|
191
208
|
end
|
192
209
|
|
193
|
-
# :nocov:
|
194
210
|
def compiled_template(file_path)
|
195
211
|
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
196
212
|
template = File.read(file_path)
|
@@ -201,7 +217,6 @@ module ViewComponent
|
|
201
217
|
handler.call(OpenStruct.new(source: template, identifier: component_class.identifier, type: component_class.type))
|
202
218
|
end
|
203
219
|
end
|
204
|
-
# :nocov:
|
205
220
|
|
206
221
|
def call_method_name(variant)
|
207
222
|
if variant.present? && variants.include?(variant)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
class SlotV2
|
5
|
+
attr_writer :_component_instance, :_content_block, :_content
|
6
|
+
|
7
|
+
def initialize(parent)
|
8
|
+
@parent = parent
|
9
|
+
end
|
10
|
+
|
11
|
+
# Used to render the slot content in the template
|
12
|
+
#
|
13
|
+
# There's currently 3 different values that may be set, that we can render.
|
14
|
+
#
|
15
|
+
# If the slot renderable is a component, the string class name of a
|
16
|
+
# component, or a function that returns a component, we render that
|
17
|
+
# component instance, returning the string.
|
18
|
+
#
|
19
|
+
# If the slot renderable is a function and returns a string, it is
|
20
|
+
# set as `@_content` and is returned directly.
|
21
|
+
#
|
22
|
+
# If there is no slot renderable, we evaluate the block passed to
|
23
|
+
# the slot and return it.
|
24
|
+
def to_s
|
25
|
+
view_context = @parent.send(:view_context)
|
26
|
+
view_context.capture do
|
27
|
+
if defined?(@_component_instance)
|
28
|
+
# render_in is faster than `parent.render`
|
29
|
+
@_component_instance.render_in(view_context, &@_content_block)
|
30
|
+
elsif defined?(@_content)
|
31
|
+
@_content
|
32
|
+
elsif defined?(@_content_block)
|
33
|
+
@_content_block.call
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Allow access to public component methods via the wrapper
|
39
|
+
#
|
40
|
+
# e.g.
|
41
|
+
#
|
42
|
+
# calling `header.name` (where `header` is a slot) will call `name`
|
43
|
+
# on the `HeaderComponent` instance.
|
44
|
+
#
|
45
|
+
# Where the component may look like:
|
46
|
+
#
|
47
|
+
# class MyComponent < ViewComponent::Base
|
48
|
+
# has_one :header, HeaderComponent
|
49
|
+
#
|
50
|
+
# class HeaderComponent < ViewComponent::Base
|
51
|
+
# def name
|
52
|
+
# @name
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
def method_missing(symbol, *args, &block)
|
58
|
+
@_component_instance.public_send(symbol, *args, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def respond_to_missing?(symbol, include_all = false)
|
62
|
+
defined?(@_component_instance) && @_component_instance.respond_to?(symbol, include_all)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -8,6 +8,12 @@ module ViewComponent
|
|
8
8
|
module Slotable
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
11
|
+
included do
|
12
|
+
# Hash of registered Slots
|
13
|
+
class_attribute :slots
|
14
|
+
self.slots = {}
|
15
|
+
end
|
16
|
+
|
11
17
|
class_methods do
|
12
18
|
# support initializing slots as:
|
13
19
|
#
|
@@ -63,6 +69,14 @@ module ViewComponent
|
|
63
69
|
}
|
64
70
|
end
|
65
71
|
end
|
72
|
+
|
73
|
+
def inherited(child)
|
74
|
+
# Clone slot configuration into child class
|
75
|
+
# see #test_slots_pollution
|
76
|
+
child.slots = self.slots.clone
|
77
|
+
|
78
|
+
super
|
79
|
+
end
|
66
80
|
end
|
67
81
|
|
68
82
|
# Build a Slot instance on a component,
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
require "view_component/slot_v2"
|
6
|
+
|
7
|
+
module ViewComponent
|
8
|
+
module SlotableV2
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
# Setup component slot state
|
12
|
+
included do
|
13
|
+
# Hash of registered Slots
|
14
|
+
class_attribute :registered_slots
|
15
|
+
self.registered_slots = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
class_methods do
|
19
|
+
##
|
20
|
+
# Registers a sub-component
|
21
|
+
#
|
22
|
+
# = Example
|
23
|
+
#
|
24
|
+
# renders_one :header -> (classes:) do
|
25
|
+
# HeaderComponent.new(classes: classes)
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # OR
|
29
|
+
#
|
30
|
+
# renders_one :header, HeaderComponent
|
31
|
+
#
|
32
|
+
# where `HeaderComponent` is defined as:
|
33
|
+
#
|
34
|
+
# class HeaderComponent < ViewComponent::Base
|
35
|
+
# def initialize(classes:)
|
36
|
+
# @classes = classes
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# and has the following template:
|
41
|
+
#
|
42
|
+
# <header class="<%= @classes %>">
|
43
|
+
# <%= content %>
|
44
|
+
# </header>
|
45
|
+
#
|
46
|
+
# = Rendering sub-component content
|
47
|
+
#
|
48
|
+
# The component's sidecar template can access the sub-component by calling a
|
49
|
+
# helper method with the same name as the sub-component.
|
50
|
+
#
|
51
|
+
# <h1>
|
52
|
+
# <%= header do %>
|
53
|
+
# My header title
|
54
|
+
# <% end %>
|
55
|
+
# </h1>
|
56
|
+
#
|
57
|
+
# = Setting sub-component content
|
58
|
+
#
|
59
|
+
# Consumers of the component can render a sub-component by calling a
|
60
|
+
# helper method with the same name as the slot.
|
61
|
+
#
|
62
|
+
# <%= render_inline(MyComponent.new) do |component| %>
|
63
|
+
# <%= component.header(classes: "Foo") do %>
|
64
|
+
# <p>Bar</p>
|
65
|
+
# <% end %>
|
66
|
+
# <% end %>
|
67
|
+
def renders_one(slot_name, callable = nil)
|
68
|
+
validate_slot_name(slot_name)
|
69
|
+
|
70
|
+
define_method slot_name do |*args, **kwargs, &block|
|
71
|
+
if args.empty? && kwargs.empty? && block.nil?
|
72
|
+
get_slot(slot_name)
|
73
|
+
else
|
74
|
+
set_slot(slot_name, *args, **kwargs, &block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
register_slot(slot_name, collection: false, callable: callable)
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Registers a collection sub-component
|
83
|
+
#
|
84
|
+
# = Example
|
85
|
+
#
|
86
|
+
# render_many :items, -> (name:) { ItemComponent.new(name: name }
|
87
|
+
#
|
88
|
+
# # OR
|
89
|
+
#
|
90
|
+
# render_many :items, ItemComponent
|
91
|
+
#
|
92
|
+
# = Rendering sub-components
|
93
|
+
#
|
94
|
+
# The component's sidecar template can access the slot by calling a
|
95
|
+
# helper method with the same name as the slot.
|
96
|
+
#
|
97
|
+
# <h1>
|
98
|
+
# <%= items.each do |item| %>
|
99
|
+
# <%= item %>
|
100
|
+
# <% end %>
|
101
|
+
# </h1>
|
102
|
+
#
|
103
|
+
# = Setting sub-component content
|
104
|
+
#
|
105
|
+
# Consumers of the component can set the content of a slot by calling a
|
106
|
+
# helper method with the same name as the slot. The method can be
|
107
|
+
# called multiple times to append to the slot.
|
108
|
+
#
|
109
|
+
# <%= render_inline(MyComponent.new) do |component| %>
|
110
|
+
# <%= component.item(name: "Foo") do %>
|
111
|
+
# <p>One</p>
|
112
|
+
# <% end %>
|
113
|
+
#
|
114
|
+
# <%= component.item(name: "Bar") do %>
|
115
|
+
# <p>two</p>
|
116
|
+
# <% end %>
|
117
|
+
# <% end %>
|
118
|
+
def renders_many(slot_name, callable = nil)
|
119
|
+
validate_slot_name(slot_name)
|
120
|
+
|
121
|
+
singular_name = ActiveSupport::Inflector.singularize(slot_name)
|
122
|
+
|
123
|
+
# Define setter for singular names
|
124
|
+
# e.g. `renders_many :items` allows fetching all tabs with
|
125
|
+
# `component.tabs` and setting a tab with `component.tab`
|
126
|
+
define_method singular_name do |*args, **kwargs, &block|
|
127
|
+
set_slot(slot_name, *args, **kwargs, &block)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Instantiates and and adds multiple slots forwarding the first
|
131
|
+
# argument to each slot constructor
|
132
|
+
define_method slot_name do |collection_args = nil, &block|
|
133
|
+
if collection_args.nil? && block.nil?
|
134
|
+
get_slot(slot_name)
|
135
|
+
else
|
136
|
+
collection_args.each do |args|
|
137
|
+
set_slot(slot_name, **args, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
register_slot(slot_name, collection: true, callable: callable)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Clone slot configuration into child class
|
146
|
+
# see #test_slots_pollution
|
147
|
+
def inherited(child)
|
148
|
+
child.registered_slots = self.registered_slots.clone
|
149
|
+
super
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def register_slot(slot_name, collection:, callable:)
|
155
|
+
# Setup basic slot data
|
156
|
+
slot = {
|
157
|
+
collection: collection,
|
158
|
+
}
|
159
|
+
# If callable responds to `render_in`, we set it on the slot as a renderable
|
160
|
+
if callable && callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
|
161
|
+
slot[:renderable] = callable
|
162
|
+
elsif callable.is_a?(String)
|
163
|
+
# If callable is a string, we assume it's referencing an internal class
|
164
|
+
slot[:renderable_class_name] = callable
|
165
|
+
elsif callable
|
166
|
+
# If slot does not respond to `render_in`, we assume it's a proc,
|
167
|
+
# define a method, and save a reference to it to call when setting
|
168
|
+
method_name = :"_call_#{slot_name}"
|
169
|
+
define_method method_name, &callable
|
170
|
+
slot[:renderable_function] = instance_method(method_name)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Register the slot on the component
|
174
|
+
self.registered_slots[slot_name] = slot
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate_slot_name(slot_name)
|
178
|
+
if self.registered_slots.key?(slot_name)
|
179
|
+
# TODO remove? This breaks overriding slots when slots are inherited
|
180
|
+
raise ArgumentError.new("#{slot_name} slot declared multiple times")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def get_slot(slot_name)
|
186
|
+
slot = self.class.registered_slots[slot_name]
|
187
|
+
@_set_slots ||= {}
|
188
|
+
|
189
|
+
if @_set_slots[slot_name]
|
190
|
+
return @_set_slots[slot_name]
|
191
|
+
end
|
192
|
+
|
193
|
+
if slot[:collection]
|
194
|
+
[]
|
195
|
+
else
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def set_slot(slot_name, *args, **kwargs, &block)
|
201
|
+
slot_definition = self.class.registered_slots[slot_name]
|
202
|
+
|
203
|
+
slot = SlotV2.new(self)
|
204
|
+
|
205
|
+
# Passing the block to the sub-component wrapper like this has two
|
206
|
+
# benefits:
|
207
|
+
#
|
208
|
+
# 1. If this is a `content_area` style sub-component, we will render the
|
209
|
+
# block via the `slot`
|
210
|
+
#
|
211
|
+
# 2. Since we have to pass block content to components when calling
|
212
|
+
# `render`, evaluating the block here would require us to call
|
213
|
+
# `view_context.capture` twice, which is slower
|
214
|
+
slot._content_block = block if block_given?
|
215
|
+
|
216
|
+
# If class
|
217
|
+
if slot_definition[:renderable]
|
218
|
+
slot._component_instance = slot_definition[:renderable].new(*args, **kwargs)
|
219
|
+
# If class name as a string
|
220
|
+
elsif slot_definition[:renderable_class_name]
|
221
|
+
slot._component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
|
222
|
+
# If passed a lambda
|
223
|
+
elsif slot_definition[:renderable_function]
|
224
|
+
# Use `bind(self)` to ensure lambda is executed in the context of the
|
225
|
+
# current component. This is necessary to allow the lambda to access helper
|
226
|
+
# methods like `content_tag` as well as parent component state.
|
227
|
+
renderable_value = slot_definition[:renderable_function].bind(self).call(*args, **kwargs, &block)
|
228
|
+
|
229
|
+
# Function calls can return components, so if it's a component handle it specially
|
230
|
+
if renderable_value.respond_to?(:render_in)
|
231
|
+
slot._component_instance = renderable_value
|
232
|
+
else
|
233
|
+
slot._content = renderable_value
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
@_set_slots ||= {}
|
238
|
+
|
239
|
+
if slot_definition[:collection]
|
240
|
+
@_set_slots[slot_name] ||= []
|
241
|
+
@_set_slots[slot_name].push(slot)
|
242
|
+
else
|
243
|
+
@_set_slots[slot_name] = slot
|
244
|
+
end
|
245
|
+
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: view_component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.25.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub Open Source
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -184,6 +184,20 @@ dependencies:
|
|
184
184
|
- - "~>"
|
185
185
|
- !ruby/object:Gem::Version
|
186
186
|
version: 0.7.2
|
187
|
+
- !ruby/object:Gem::Dependency
|
188
|
+
name: pry
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - "~>"
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0.13'
|
194
|
+
type: :development
|
195
|
+
prerelease: false
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - "~>"
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0.13'
|
187
201
|
description:
|
188
202
|
email:
|
189
203
|
- opensource+view_component@github.com
|
@@ -205,6 +219,8 @@ files:
|
|
205
219
|
- lib/rails/generators/erb/templates/component.html.erb.tt
|
206
220
|
- lib/rails/generators/haml/component_generator.rb
|
207
221
|
- lib/rails/generators/haml/templates/component.html.haml.tt
|
222
|
+
- lib/rails/generators/preview/component_generator.rb
|
223
|
+
- lib/rails/generators/preview/templates/component_preview.rb.tt
|
208
224
|
- lib/rails/generators/rspec/component_generator.rb
|
209
225
|
- lib/rails/generators/rspec/templates/component_spec.rb.tt
|
210
226
|
- lib/rails/generators/slim/component_generator.rb
|
@@ -227,7 +243,9 @@ files:
|
|
227
243
|
- lib/view_component/rendering_component_helper.rb
|
228
244
|
- lib/view_component/rendering_monkey_patch.rb
|
229
245
|
- lib/view_component/slot.rb
|
246
|
+
- lib/view_component/slot_v2.rb
|
230
247
|
- lib/view_component/slotable.rb
|
248
|
+
- lib/view_component/slotable_v2.rb
|
231
249
|
- lib/view_component/template_error.rb
|
232
250
|
- lib/view_component/test_case.rb
|
233
251
|
- lib/view_component/test_helpers.rb
|
@@ -252,7 +270,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
252
270
|
- !ruby/object:Gem::Version
|
253
271
|
version: '0'
|
254
272
|
requirements: []
|
255
|
-
rubygems_version: 3.
|
273
|
+
rubygems_version: 3.0.3
|
256
274
|
signing_key:
|
257
275
|
specification_version: 4
|
258
276
|
summary: View components for Rails
|