view_component 2.22.0 → 2.24.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: 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