view_component 2.22.1 → 2.23.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 443af024d4cbb4251192a8d21eb95d40ad011aaac81a6572acfade86fd237f2d
4
- data.tar.gz: bf7768afd857d6ed5580105a947fb5329615b01513f748c7856e33cd1cc43af9
3
+ metadata.gz: fcba3738e1cef136c14cd82cc3dd8301596f576b8891bd668e5216ff0dca9bc6
4
+ data.tar.gz: 7bd2112814c640f4b524f557799d1484fcaf88240505e7e09753cdacf7af87bd
5
5
  SHA512:
6
- metadata.gz: 3f04ddd1e8c82e8b7bf586eb249a0c8dcbdf6a3b214c9b26fabf988d2c1696f78cd684654d8a7ffc7d86bf8e282204a8643784bc1c8b0153338b688b8cfe252f
7
- data.tar.gz: 4b7d2fa52955349650500493c90882b05809b74fb447d590ea96aea9781198233dd091db7db1b4a643c4185830f9ff10b8e5a44dd2c48ccb3c9f8c42ad0525ea
6
+ metadata.gz: e97c8b9fe11d78da84c129f99bfa19c8e76596064d76adc78f28127410f7f5b31a856c31a4a1e8444c707d7d228b1ffe4c01d9b70ffcc54a3ef9bd7edba7a61b
7
+ data.tar.gz: 6856e490404554040459f57bfab1ae523a00808498230078bc914f97f1bcaf19d1f4a50c5ba66c5d31b0250d2d3185df0f4a93b6761bd875349e0b3a7f643466
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.23.0
6
+
7
+ * Add `ActionView::SlotableV2`
8
+ * `with_slot` becomes `renders_one`.
9
+ * `with_slot collection: true` becomes `renders_many`.
10
+ * Slot definitions now accept either a component class, component class name, or a lambda instead of a `class_name:` keyword argument.
11
+ * Slots now support positional arguments.
12
+ * Slots no longer use the `content` attribute to render content, instead relying on `to_s`. e.g. `<%= my_slot %>`.
13
+ * Slot values are no longer set via the `slot` method, and instead use the name of the slot.
14
+
15
+ *Blake Williams*
16
+
17
+ * Add `frozen_string_literal: true` to generated component template.
18
+
19
+ *Max Beizer*
20
+
5
21
  ## 2.22.1
6
22
 
7
23
  * Revert refactor that broke rendering for some users.
@@ -1,3 +1,5 @@
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 %>)
@@ -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
@@ -20,10 +21,6 @@ module ViewComponent
20
21
  class_attribute :content_areas
21
22
  self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
22
23
 
23
- # Hash of registered Slots
24
- class_attribute :slots
25
- self.slots = {}
26
-
27
24
  # Entrypoint for rendering components.
28
25
  #
29
26
  # view_context: ActionView context from calling view
@@ -99,13 +96,16 @@ module ViewComponent
99
96
 
100
97
  def initialize(*); end
101
98
 
102
- # If trying to render a partial or template inside a component,
103
- # pass the render call to the parent view_context.
99
+ # Re-use original view_context if we're not rendering a component.
100
+ #
101
+ # This prevents an exception when rendering a partial inside of a component that has also been rendered outside
102
+ # of the component. This is due to the partials compiled template method existing in the parent `view_context`,
103
+ # and not the component's `view_context`.
104
104
  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
105
+ if options.is_a? ViewComponent::Base
108
106
  super
107
+ else
108
+ view_context.render(options, args, &block)
109
109
  end
110
110
  end
111
111
 
@@ -132,7 +132,10 @@ module ViewComponent
132
132
 
133
133
  # For caching, such as #cache_if
134
134
  def format
135
- @variant
135
+ # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
136
+ if defined?(@variant)
137
+ @variant
138
+ end
136
139
  end
137
140
 
138
141
  # Assign the provided content to the content area accessor
@@ -207,10 +210,6 @@ module ViewComponent
207
210
  # Removes the first part of the path and the extension.
208
211
  child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
209
212
 
210
- # Clone slot configuration into child class
211
- # see #test_slots_pollution
212
- child.slots = self.slots.clone
213
-
214
213
  super
215
214
  end
216
215
 
@@ -291,7 +290,6 @@ module ViewComponent
291
290
  def provided_collection_parameter
292
291
  @provided_collection_parameter ||= nil
293
292
  end
294
-
295
293
  end
296
294
 
297
295
  ActiveSupport.run_load_hooks(:view_component, self)
@@ -146,21 +146,36 @@ module ViewComponent
146
146
  source_location = component_class.source_location
147
147
  return [] unless source_location
148
148
 
149
- location_without_extension = source_location.chomp(File.extname(source_location))
150
-
151
149
  extensions = ActionView::Template.template_handler_extensions.join(",")
152
150
 
153
- # view files in the same directory as the component
154
- sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
155
-
156
151
  # view files in a directory named like the component
157
152
  directory = File.dirname(source_location)
158
153
  filename = File.basename(source_location, ".rb")
159
154
  component_name = component_class.name.demodulize.underscore
160
155
 
156
+ # Add support for nested components defined in the same file.
157
+ #
158
+ # e.g.
159
+ #
160
+ # class MyComponent < ViewComponent::Base
161
+ # class MyOtherComponent < ViewComponent::Base
162
+ # end
163
+ # end
164
+ #
165
+ # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
166
+ nested_component_files = if component_class.name.include?("::")
167
+ nested_component_path = component_class.name.deconstantize.underscore
168
+ Dir["#{directory}/#{nested_component_path}/#{component_name}.*{#{extensions}}"]
169
+ else
170
+ []
171
+ end
172
+
173
+ # view files in the same directory as the component
174
+ sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
175
+
161
176
  sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
162
177
 
163
- (sidecar_files - [source_location] + sidecar_directory_files)
178
+ (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files)
164
179
  end
165
180
 
166
181
  def inline_calls
@@ -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
+ if defined?(@_component_instance)
26
+ # render_in is faster than `parent.render`
27
+ @_component_instance.render_in(
28
+ @parent.send(:view_context),
29
+ &@_content_block
30
+ )
31
+ elsif defined?(@_content)
32
+ @_content
33
+ elsif defined?(@_content_block)
34
+ @_content_block.call
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
+ @_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 = 23
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.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-10 00:00:00.000000000 Z
11
+ date: 2020-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -184,7 +184,21 @@ dependencies:
184
184
  - - "~>"
185
185
  - !ruby/object:Gem::Version
186
186
  version: 0.7.2
187
- description:
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'
201
+ description:
188
202
  email:
189
203
  - opensource+view_component@github.com
190
204
  executables: []
@@ -227,7 +241,9 @@ files:
227
241
  - lib/view_component/rendering_component_helper.rb
228
242
  - lib/view_component/rendering_monkey_patch.rb
229
243
  - lib/view_component/slot.rb
244
+ - lib/view_component/slot_v2.rb
230
245
  - lib/view_component/slotable.rb
246
+ - lib/view_component/slotable_v2.rb
231
247
  - lib/view_component/template_error.rb
232
248
  - lib/view_component/test_case.rb
233
249
  - lib/view_component/test_helpers.rb
@@ -237,7 +253,7 @@ licenses:
237
253
  - MIT
238
254
  metadata:
239
255
  allowed_push_host: https://rubygems.org
240
- post_install_message:
256
+ post_install_message:
241
257
  rdoc_options: []
242
258
  require_paths:
243
259
  - lib
@@ -253,7 +269,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
269
  version: '0'
254
270
  requirements: []
255
271
  rubygems_version: 3.1.2
256
- signing_key:
272
+ signing_key:
257
273
  specification_version: 4
258
274
  summary: View components for Rails
259
275
  test_files: []