view_component 2.22.0 → 2.24.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: a9c3a94a5583fa593efd0d7f90b0bcbf48fb0436c05233594b6ff41dba6a52cc
4
- data.tar.gz: f859ddf3a59cc181215ea3b7b7e848f08cd8d791b59896512633ba2533f53b5a
3
+ metadata.gz: 28b088083cd9a83d90264715da7105bbc83e4c37e81785db5ca11c8192c6ccf6
4
+ data.tar.gz: d6ebc8a6a830e7bcf2490457d471017b9d123705e55ec9f6e0b1d2745e6a99c4
5
5
  SHA512:
6
- metadata.gz: 70d5b2e41eaa8b192246a85d91ca0a269f0050b296050edb93eda910e7df3249a2784f6518f35285862786b1ef51f749677f502897261c098f4d5e29365f3f38
7
- data.tar.gz: 1012abb02ab64ab80afbe8143abe262a16bdef606b50af4beecc34c554ff7ee327228512a9f5ed85e625e72511fa26b3b48ce5aaabec7d5a183453e33191cc7d
6
+ metadata.gz: 9061290b38e66429e6f66327082d17f2fb8d26ef1022557a049a642335d4af79e6dcf05797a6af9ba95eb074315d3d1272c4c6902e87aa87728fc01fd694fe62
7
+ data.tar.gz: 5bcf720d202c0fbbeea76d9dbd044239dfb777be51ba47d39965aef104f84af5372aecf3d99bf203f973773bce3b311e41716b3e7715e8bd4b9175b3c305740a
@@ -1,6 +1,50 @@
1
1
  # CHANGELOG
2
2
 
3
- ## master
3
+ ## main
4
+
5
+ ## 2.24.0
6
+
7
+ * Add test case for checking presence of `content` in `#render?`.
8
+
9
+ *Joel Hawksley*
10
+
11
+ * Rename `master` branch to `main`.
12
+
13
+ *Joel Hawksley*
14
+
15
+ ## 2.23.2
16
+
17
+ * Fix bug where rendering a component `with_collection` from a controller raised an error.
18
+
19
+ *Joel Hawksley*
20
+
21
+ ## 2.23.1
22
+
23
+ * Fixed out-of-order rendering bug in `ActionView::SlotableV2`
24
+
25
+ *Blake Williams*
26
+
27
+ ## 2.23.0
28
+
29
+ * Add `ActionView::SlotableV2`
30
+ * `with_slot` becomes `renders_one`.
31
+ * `with_slot collection: true` becomes `renders_many`.
32
+ * Slot definitions now accept either a component class, component class name, or a lambda instead of a `class_name:` keyword argument.
33
+ * Slots now support positional arguments.
34
+ * Slots no longer use the `content` attribute to render content, instead relying on `to_s`. e.g. `<%= my_slot %>`.
35
+ * Slot values are no longer set via the `slot` method, and instead use the name of the slot.
36
+
37
+ *Blake Williams*
38
+
39
+ * Add `frozen_string_literal: true` to generated component template.
40
+
41
+ *Max Beizer*
42
+
43
+ ## 2.22.1
44
+
45
+ * Revert refactor that broke rendering for some users.
46
+
47
+ *Joel Hawksley*
4
48
 
5
49
  ## 2.22.0
6
50
 
@@ -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,10 +96,17 @@ 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
- view_context.render(options, args, &block)
105
+ if options.is_a? ViewComponent::Base
106
+ super
107
+ else
108
+ view_context.render(options, args, &block)
109
+ end
106
110
  end
107
111
 
108
112
  def controller
@@ -113,7 +117,7 @@ module ViewComponent
113
117
  # Provides a proxy to access helper methods from the context of the current controller
114
118
  def helpers
115
119
  raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
116
- @helpers ||= view_context
120
+ @helpers ||= controller.view_context
117
121
  end
118
122
 
119
123
  # Exposes .virutal_path as an instance method
@@ -128,7 +132,10 @@ module ViewComponent
128
132
 
129
133
  # For caching, such as #cache_if
130
134
  def format
131
- @variant
135
+ # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
136
+ if defined?(@variant)
137
+ @variant
138
+ end
132
139
  end
133
140
 
134
141
  # Assign the provided content to the content area accessor
@@ -203,10 +210,6 @@ module ViewComponent
203
210
  # Removes the first part of the path and the extension.
204
211
  child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
205
212
 
206
- # Clone slot configuration into child class
207
- # see #test_slots_pollution
208
- child.slots = self.slots.clone
209
-
210
213
  super
211
214
  end
212
215
 
@@ -287,7 +290,6 @@ module ViewComponent
287
290
  def provided_collection_parameter
288
291
  @provided_collection_parameter ||= nil
289
292
  end
290
-
291
293
  end
292
294
 
293
295
  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
@@ -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
+ 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,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 22
6
+ MINOR = 24
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
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.0
4
+ version: 2.24.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-09 00:00:00.000000000 Z
11
+ date: 2020-12-23 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
@@ -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