view_component 2.22.1 → 2.23.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: 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: []