view_component 2.22.1 → 2.25.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 443af024d4cbb4251192a8d21eb95d40ad011aaac81a6572acfade86fd237f2d
4
- data.tar.gz: bf7768afd857d6ed5580105a947fb5329615b01513f748c7856e33cd1cc43af9
3
+ metadata.gz: ed1ee878b4b1519adcb75b06689a3341367cca5aa8ae4dd087cb2fba0c06a355
4
+ data.tar.gz: 70459f53b555b8edf6675346a42c1be87bb36267bce23ffb9cc4a639731c1b77
5
5
  SHA512:
6
- metadata.gz: 3f04ddd1e8c82e8b7bf586eb249a0c8dcbdf6a3b214c9b26fabf988d2c1696f78cd684654d8a7ffc7d86bf8e282204a8643784bc1c8b0153338b688b8cfe252f
7
- data.tar.gz: 4b7d2fa52955349650500493c90882b05809b74fb447d590ea96aea9781198233dd091db7db1b4a643c4185830f9ff10b8e5a44dd2c48ccb3c9f8c42ad0525ea
6
+ metadata.gz: 2ac2189345c278608e8dc96ec1c7f90472991aefc86da43968489f3ea3cff4c9306ef8280553b2dad6a7a35b24e74eabd2269b3efcba4026b00b642dcac0dcf4
7
+ data.tar.gz: 6a393762db37291a6f6eaa2e19575aa035f103cc6c0fe71c96648fb827ed0e140c0e9d9f297901f699f7abd3e4946ca8e048bf53b1dbe62e47663e2bb591918e
@@ -1,6 +1,58 @@
1
1
  # CHANGELOG
2
2
 
3
- ## master
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["sidecar"]
19
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.erb")
20
- else
21
- File.join("app/components", class_path, "#{file_name}_component.html.erb")
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
@@ -0,0 +1,5 @@
1
+ class <%= class_name %>ComponentPreview < ViewComponent::Preview
2
+ def default
3
+ render(<%= class_name %>Component.new)
4
+ end
5
+ end
@@ -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
- # If trying to render a partial or template inside a component,
103
- # pass the render call to the parent view_context.
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?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
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
- @variant
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 initialize_parameters.map(&:last).include?(parameter)
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
- @component.compile(raise_errors: true)
11
- @component.validate_collection_parameter!(validate_default: true)
13
+ component.compile(raise_errors: true)
14
+ component.validate_collection_parameter!(validate_default: true)
12
15
 
13
16
  @collection.map do |item|
14
- content = @component.new(**component_options(item, iterator)).render_in(view_context, &block)
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 = { @component.collection_parameter => item }
38
- item_options[@component.collection_counter_parameter] = iterator.index + 1 if @component.counter_argument_present?
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
- component_class.validate_collection_parameter! if raise_errors
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
@@ -3,8 +3,8 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 22
7
- PATCH = 1
6
+ MINOR = 25
7
+ PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
10
10
  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.22.1
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: 2020-11-10 00:00:00.000000000 Z
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.1.2
273
+ rubygems_version: 3.0.3
256
274
  signing_key:
257
275
  specification_version: 4
258
276
  summary: View components for Rails