view_component 2.28.0 → 2.32.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: 0abc25e32e2befb60487f209469b2d9b96d6f1380a345466bcc816addc9f65d1
4
- data.tar.gz: a72e8ad8a5cfa1bbfd0bbbaa5510e14abd4818657b356681930537d8d5efa95e
3
+ metadata.gz: 0cdb5a57cbe401fab359e9daedfcb0180b2e6ec07bd332e7616c6a611442df13
4
+ data.tar.gz: 11ec2515ac60b615adc2a52cc7b526c7632871363a24a7b3128ca9a4873d57a1
5
5
  SHA512:
6
- metadata.gz: 0b3f1cd9a86493fd5fbf68537536435c13b202bcba0d01e8d58f20ba0c158b7cdf1f878c75103d4b6a00f54e89227f7cb6b083bbbab17122e3e7f6b9d7c7f20d
7
- data.tar.gz: 4a29369a237c75889bce3a0777e5ff82e2bd7d25966ea31004fb293db5a6015aed2abd618340504ee89481d43970a35ff05c3aec2e113ff8b6378ed39415dfec
6
+ metadata.gz: ba44487de1f76102ec0fa9ea4fdd4af85c079c665c0e879855af5fe57577878a176badfef0af2a3d06d3384e852986048ab1332ed0a78fc589adea83f087b037
7
+ data.tar.gz: 32b565ca3c4759b366acf47de7f42a74ec52adb96d951013759cb5a289ffc16956b432bbee8e0dd79c3291c16386655fd7b8332da2ad3ba29db9dadcc87456a9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,101 @@
2
2
 
3
3
  ## main
4
4
 
5
+ ## 2.32.0
6
+
7
+ * Enable previews by default in test environment.
8
+
9
+ *Edouard Piron*
10
+
11
+ * Fix test helper compatibility with Rails 7.0, TestRequest, and TestSession.
12
+
13
+ *Leo Correa*
14
+
15
+ * Add experimental `_output_postamble` lifecyle method.
16
+
17
+ *Joel Hawksley*
18
+
19
+ * Add compatibility notes on FAQ.
20
+
21
+ *Matheus Richard*
22
+
23
+ * Add Bridgetown on Compatibility documentation.
24
+
25
+ *Matheus Richard*
26
+
27
+ * Are you interested in building the future of ViewComponent? GitHub is looking to hire a Senior Engineer to work on Primer ViewComponents and ViewComponent. Apply here: [US/Canada](https://github.com/careers) / [Europe](https://boards.greenhouse.io/github/jobs/3132294). Feel free to reach out to joelhawksley@github.com with any questions.
28
+
29
+ *Joel Hawksley*
30
+
31
+ ## 2.31.1
32
+
33
+ * Fix `DEPRECATION WARNING: before_render_check` when compiling `ViewComponent::Base`
34
+
35
+ *Dave Kroondyk*
36
+
37
+ ## 2.31.0
38
+
39
+ _Note: This release includes an underlying change to Slots that may affect incorrect usage of the API, where Slots were set on a line prefixed by `<%=`. The result of setting a Slot should not be returned. (`<%`)_
40
+
41
+ * Add `#with_content` to allow setting content without a block.
42
+
43
+ *Jordan Raine, Manuel Puyol*
44
+
45
+ * Add `with_request_url` test helper.
46
+
47
+ *Mario Schüttel*
48
+
49
+ * Improve feature parity with Rails translations
50
+ * Don't create a translation backend if the component has no translation file
51
+ * Mark translation keys ending with `html` as HTML-safe
52
+ * Always convert keys to String
53
+ * Support multiple keys
54
+
55
+ *Elia Schito*
56
+
57
+ * Fix errors on `asset_url` helpers when `asset_host` has no protocol.
58
+
59
+ *Elia Schito*
60
+
61
+ * Prevent slots from overriding the `#content` method when registering a slot with that name.
62
+
63
+ *Blake Williams*
64
+
65
+ * Deprecate `with_slot` in favor of the new [slots API](https://viewcomponent.org/guide/slots.html).
66
+
67
+ *Manuel Puyol*
68
+
69
+ ## 2.30.0
70
+
71
+ * Deprecate `with_content_areas` in favor of [slots](https://viewcomponent.org/guide/slots.html).
72
+
73
+ *Joel Hawksley*
74
+
75
+ ## 2.29.0
76
+
77
+ * Allow Slot lambdas to share data from the parent component and allow chaining on the returned component.
78
+
79
+ *Sjors Baltus, Blake Williams*
80
+
81
+ * Experimental: Add `ViewComponent::Translatable`
82
+ * `t` and `translate` now will look first into the sidecar YAML translations file.
83
+ * `helpers.t` and `I18n.t` still reference the global Rails translation files.
84
+ * `l` and `localize` will still reference the global Rails translation files.
85
+
86
+ *Elia Schito*
87
+
88
+ * Fix rendering output of pass through slots when using HAML.
89
+
90
+ *Alex Robbin, Blake Williams*
91
+
92
+ * Experimental: call `._sidecar_files` to fetch the sidecar files for a given list of extensions, e.g. passing `["yml", "yaml"]`.
93
+
94
+ *Elia Schito*
95
+
96
+ * Fix bug where a single `jbuilder` template matched multiple template handlers.
97
+
98
+ *Niels Slot*
99
+
5
100
  ## 2.28.0
6
101
 
7
102
  * Include SlotableV2 by default in Base. **Note:** It's no longer necessary to include `ViewComponent::SlotableV2` to use Slots.
data/README.md CHANGED
@@ -6,14 +6,6 @@ A framework for building reusable, testable & encapsulated view components in Ru
6
6
 
7
7
  See [viewcomponent.org](https://viewcomponent.org/) for documentation.
8
8
 
9
- ## Installation
10
-
11
- In `Gemfile`, add:
12
-
13
- ```ruby
14
- gem "view_component", require: "view_component/engine"
15
- ```
16
-
17
9
  ## Contributing
18
10
 
19
11
  This project is intended to be a safe, welcoming space for collaboration. Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. We recommend reading the [contributing guide](./CONTRIBUTING.md) as well.
@@ -7,9 +7,11 @@ module ViewComponent
7
7
 
8
8
  autoload :Base
9
9
  autoload :Compiler
10
+ autoload :ComponentError
10
11
  autoload :Preview
11
12
  autoload :PreviewTemplateError
12
13
  autoload :TestHelpers
13
14
  autoload :TestCase
14
15
  autoload :TemplateError
16
+ autoload :Translatable
15
17
  end
@@ -7,12 +7,14 @@ require "view_component/compile_cache"
7
7
  require "view_component/previewable"
8
8
  require "view_component/slotable"
9
9
  require "view_component/slotable_v2"
10
+ require "view_component/with_content_helper"
10
11
 
11
12
  module ViewComponent
12
13
  class Base < ActionView::Base
13
14
  include ActiveSupport::Configurable
14
15
  include ViewComponent::Previewable
15
16
  include ViewComponent::SlotableV2
17
+ include ViewComponent::WithContentHelper
16
18
 
17
19
  ViewContextCalledBeforeRenderError = Class.new(StandardError)
18
20
 
@@ -28,6 +30,7 @@ module ViewComponent
28
30
  # Hook for allowing components to do work as part of the compilation process.
29
31
  #
30
32
  # For example, one might compile component-specific assets at this point.
33
+ # @private TODO: add documentation
31
34
  def self._after_compile
32
35
  # noop
33
36
  end
@@ -56,6 +59,7 @@ module ViewComponent
56
59
  # returns:
57
60
  # <span title="greeting">Hello, world!</span>
58
61
  #
62
+ # @private
59
63
  def render_in(view_context, &block)
60
64
  self.class.compile(raise_errors: true)
61
65
 
@@ -79,13 +83,15 @@ module ViewComponent
79
83
  old_current_template = @current_template
80
84
  @current_template = self
81
85
 
86
+ raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if block && defined?(@_content_set_by_with_content)
87
+
82
88
  @_content_evaluated = false
83
89
  @_render_in_block = block
84
90
 
85
91
  before_render
86
92
 
87
93
  if render?
88
- render_template_for(@variant)
94
+ render_template_for(@variant) + _output_postamble
89
95
  else
90
96
  ""
91
97
  end
@@ -93,18 +99,36 @@ module ViewComponent
93
99
  @current_template = old_current_template
94
100
  end
95
101
 
102
+ # EXPERIMENTAL: Optional content to be returned after the rendered template.
103
+ #
104
+ # @return [String]
105
+ def _output_postamble
106
+ ""
107
+ end
108
+
109
+ # Called before rendering the component. Override to perform operations that depend on having access to the view context, such as helpers.
110
+ #
111
+ # @return [void]
96
112
  def before_render
97
113
  before_render_check
98
114
  end
99
115
 
116
+ # Called after rendering the component.
117
+ #
118
+ # @deprecated Use `before_render` instead. Will be removed in v3.0.0.
119
+ # @return [void]
100
120
  def before_render_check
101
121
  # noop
102
122
  end
103
123
 
124
+ # Override to determine whether the ViewComponent should render.
125
+ #
126
+ # @return [Boolean]
104
127
  def render?
105
128
  true
106
129
  end
107
130
 
131
+ # @private
108
132
  def initialize(*); end
109
133
 
110
134
  # Re-use original view_context if we're not rendering a component.
@@ -112,6 +136,7 @@ module ViewComponent
112
136
  # This prevents an exception when rendering a partial inside of a component that has also been rendered outside
113
137
  # of the component. This is due to the partials compiled template method existing in the parent `view_context`,
114
138
  # and not the component's `view_context`.
139
+ # @private
115
140
  def render(options = {}, args = {}, &block)
116
141
  if options.is_a? ViewComponent::Base
117
142
  super
@@ -120,28 +145,36 @@ module ViewComponent
120
145
  end
121
146
  end
122
147
 
148
+ # The current controller. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
149
+ #
150
+ # @return [ActionController::Base]
123
151
  def controller
124
152
  raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil?
125
153
  @controller ||= view_context.controller
126
154
  end
127
155
 
128
- # Provides a proxy to access helper methods from the context of the current controller
156
+ # A proxy through which to access helpers. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
157
+ #
158
+ # @return [ActionView::Base]
129
159
  def helpers
130
160
  raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
131
161
  @helpers ||= controller.view_context
132
162
  end
133
163
 
134
- # Exposes .virutal_path as an instance method
164
+ # Exposes .virtual_path as an instance method
165
+ # @private
135
166
  def virtual_path
136
167
  self.class.virtual_path
137
168
  end
138
169
 
139
170
  # For caching, such as #cache_if
171
+ # @private
140
172
  def view_cache_dependencies
141
173
  []
142
174
  end
143
175
 
144
176
  # For caching, such as #cache_if
177
+ # @private
145
178
  def format
146
179
  # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
147
180
  if defined?(@variant)
@@ -150,6 +183,7 @@ module ViewComponent
150
183
  end
151
184
 
152
185
  # Assign the provided content to the content area accessor
186
+ # @private
153
187
  def with(area, content = nil, &block)
154
188
  unless content_areas.include?(area)
155
189
  raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
@@ -163,21 +197,22 @@ module ViewComponent
163
197
  nil
164
198
  end
165
199
 
200
+ # @private TODO: add documentation
166
201
  def with_variant(variant)
167
202
  @variant = variant
168
203
 
169
204
  self
170
205
  end
171
206
 
172
- private
173
-
174
- # Exposes the current request to the component.
175
- # Use sparingly as doing so introduces coupling
176
- # that inhibits encapsulation & reuse.
207
+ # The current request. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
208
+ #
209
+ # @return [ActionDispatch::Request]
177
210
  def request
178
211
  @request ||= controller.request
179
212
  end
180
213
 
214
+ private
215
+
181
216
  attr_reader :view_context
182
217
 
183
218
  def content
@@ -186,6 +221,8 @@ module ViewComponent
186
221
 
187
222
  @_content = if @view_context && @_render_in_block
188
223
  view_context.capture(self, &@_render_in_block)
224
+ elsif defined?(@_content_set_by_with_content)
225
+ @_content_set_by_with_content
189
226
  end
190
227
  end
191
228
 
@@ -206,6 +243,47 @@ module ViewComponent
206
243
  class << self
207
244
  attr_accessor :source_location, :virtual_path
208
245
 
246
+ # EXPERIMENTAL: This API is experimental and may be removed at any time.
247
+ # Find sidecar files for the given extensions.
248
+ #
249
+ # The provided array of extensions is expected to contain
250
+ # strings starting without the "dot", example: `["erb", "haml"]`.
251
+ #
252
+ # For example, one might collect sidecar CSS files that need to be compiled.
253
+ def _sidecar_files(extensions)
254
+ return [] unless source_location
255
+
256
+ extensions = extensions.join(",")
257
+
258
+ # view files in a directory named like the component
259
+ directory = File.dirname(source_location)
260
+ filename = File.basename(source_location, ".rb")
261
+ component_name = name.demodulize.underscore
262
+
263
+ # Add support for nested components defined in the same file.
264
+ #
265
+ # e.g.
266
+ #
267
+ # class MyComponent < ViewComponent::Base
268
+ # class MyOtherComponent < ViewComponent::Base
269
+ # end
270
+ # end
271
+ #
272
+ # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
273
+ nested_component_files = if name.include?("::") && component_name != filename
274
+ Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
275
+ else
276
+ []
277
+ end
278
+
279
+ # view files in the same directory as the component
280
+ sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
281
+
282
+ sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
283
+
284
+ (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
285
+ end
286
+
209
287
  # Render a component collection.
210
288
  def with_collection(collection, **args)
211
289
  Collection.new(self, collection, **args)
@@ -239,7 +317,7 @@ module ViewComponent
239
317
  end
240
318
 
241
319
  def compiled?
242
- template_compiler.compiled?
320
+ compiler.compiled?
243
321
  end
244
322
 
245
323
  # Compile templates to instance methods, assuming they haven't been compiled already.
@@ -247,11 +325,11 @@ module ViewComponent
247
325
  # Do as much work as possible in this step, as doing so reduces the amount
248
326
  # of work done each time a component is rendered.
249
327
  def compile(raise_errors: false)
250
- template_compiler.compile(raise_errors: raise_errors)
328
+ compiler.compile(raise_errors: raise_errors)
251
329
  end
252
330
 
253
- def template_compiler
254
- @_template_compiler ||= Compiler.new(self)
331
+ def compiler
332
+ @_compiler ||= Compiler.new(self)
255
333
  end
256
334
 
257
335
  # we'll eventually want to update this to support other types
@@ -268,6 +346,11 @@ module ViewComponent
268
346
  end
269
347
 
270
348
  def with_content_areas(*areas)
349
+ ActiveSupport::Deprecation.warn(
350
+ "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
351
+ "Use slots (https://viewcomponent.org/guide/slots.html) instead."
352
+ )
353
+
271
354
  if areas.include?(:content)
272
355
  raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
273
356
  end
@@ -319,7 +402,7 @@ module ViewComponent
319
402
  def validate_initialization_parameters!
320
403
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
321
404
 
322
- raise ArgumentError.new(
405
+ raise ViewComponent::ComponentError.new(
323
406
  "#{self} initializer cannot contain " \
324
407
  "`#{RESERVED_PARAMETER}` since it will override a " \
325
408
  "public ViewComponent method."
@@ -339,7 +422,7 @@ module ViewComponent
339
422
  end
340
423
 
341
424
  def counter_argument_present?
342
- instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
425
+ initialize_parameter_names.include?(collection_counter_parameter)
343
426
  end
344
427
 
345
428
  private
@@ -13,12 +13,18 @@ module ViewComponent
13
13
  def compile(raise_errors: false)
14
14
  return if compiled?
15
15
 
16
+ subclass_instance_methods = component_class.instance_methods(false)
17
+
18
+ if subclass_instance_methods.include?(:with_content) && raise_errors
19
+ raise ViewComponent::ComponentError.new("#{component_class} implements a reserved method, `with_content`.")
20
+ end
21
+
16
22
  if template_errors.present?
17
23
  raise ViewComponent::TemplateError.new(template_errors) if raise_errors
18
24
  return false
19
25
  end
20
26
 
21
- if component_class.instance_methods(false).include?(:before_render_check)
27
+ if subclass_instance_methods.include?(:before_render_check)
22
28
  ActiveSupport::Deprecation.warn(
23
29
  "`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
24
30
  )
@@ -114,50 +120,18 @@ module ViewComponent
114
120
  end
115
121
 
116
122
  def templates
117
- @templates ||= matching_views_in_source_location.each_with_object([]) do |path, memo|
118
- pieces = File.basename(path).split(".")
119
-
120
- memo << {
121
- path: path,
122
- variant: pieces.second.split("+").second&.to_sym,
123
- handler: pieces.last
124
- }
125
- end
126
- end
127
-
128
- def matching_views_in_source_location
129
- source_location = component_class.source_location
130
- return [] unless source_location
131
-
132
- extensions = ActionView::Template.template_handler_extensions.join(",")
133
-
134
- # view files in a directory named like the component
135
- directory = File.dirname(source_location)
136
- filename = File.basename(source_location, ".rb")
137
- component_name = component_class.name.demodulize.underscore
138
-
139
- # Add support for nested components defined in the same file.
140
- #
141
- # e.g.
142
- #
143
- # class MyComponent < ViewComponent::Base
144
- # class MyOtherComponent < ViewComponent::Base
145
- # end
146
- # end
147
- #
148
- # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
149
- nested_component_files = if component_class.name.include?("::") && component_name != filename
150
- Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
151
- else
152
- []
123
+ @templates ||= begin
124
+ extensions = ActionView::Template.template_handler_extensions
125
+
126
+ component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
127
+ pieces = File.basename(path).split(".")
128
+ memo << {
129
+ path: path,
130
+ variant: pieces.second.split("+").second&.to_sym,
131
+ handler: pieces.last
132
+ }
133
+ end
153
134
  end
154
-
155
- # view files in the same directory as the component
156
- sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
157
-
158
- sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
159
-
160
- (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files)
161
135
  end
162
136
 
163
137
  def inline_calls
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ class ComponentError < StandardError
5
+ end
6
+ end
@@ -12,7 +12,7 @@ module ViewComponent
12
12
  options = app.config.view_component
13
13
 
14
14
  options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
15
- options.show_previews = Rails.env.development? if options.show_previews.nil?
15
+ options.show_previews = Rails.env.development? || Rails.env.test? if options.show_previews.nil?
16
16
  options.preview_route ||= ViewComponent::Base.preview_route
17
17
  options.preview_controller ||= ViewComponent::Base.preview_controller
18
18
 
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "view_component/with_content_helper"
4
+
3
5
  module ViewComponent
4
6
  class SlotV2
7
+ include ViewComponent::WithContentHelper
8
+
5
9
  attr_writer :_component_instance, :_content_block, :_content
6
10
 
7
11
  def initialize(parent)
@@ -25,19 +29,32 @@ module ViewComponent
25
29
  return @content if defined?(@content)
26
30
 
27
31
  view_context = @parent.send(:view_context)
28
- @content = view_context.capture do
29
- if defined?(@_component_instance)
30
- # render_in is faster than `parent.render`
31
- if defined?(@_content_block)
32
- @_component_instance.render_in(view_context, &@_content_block)
33
- else
32
+
33
+ raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if defined?(@_content_block) && defined?(@_content_set_by_with_content)
34
+
35
+ @content = if defined?(@_component_instance)
36
+ if defined?(@_content_set_by_with_content)
37
+ @_component_instance.with_content(@_content_set_by_with_content)
38
+
39
+ view_context.capture do
34
40
  @_component_instance.render_in(view_context)
35
41
  end
36
- elsif defined?(@_content)
37
- @_content
38
42
  elsif defined?(@_content_block)
39
- @_content_block.call
43
+ view_context.capture do
44
+ # render_in is faster than `parent.render`
45
+ @_component_instance.render_in(view_context, &@_content_block)
46
+ end
47
+ else
48
+ view_context.capture do
49
+ @_component_instance.render_in(view_context)
50
+ end
40
51
  end
52
+ elsif defined?(@_content)
53
+ @_content
54
+ elsif defined?(@_content_block)
55
+ view_context.capture(&@_content_block)
56
+ elsif defined?(@_content_set_by_with_content)
57
+ @_content_set_by_with_content
41
58
  end
42
59
 
43
60
  @content
@@ -23,6 +23,11 @@ module ViewComponent
23
23
  # class_name: "Header" # class name string, used to instantiate Slot
24
24
  # )
25
25
  def with_slot(*slot_names, collection: false, class_name: nil)
26
+ ActiveSupport::Deprecation.warn(
27
+ "`with_slot` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
28
+ "Use the new slots API (https://viewcomponent.org/guide/slots.html) instead."
29
+ )
30
+
26
31
  slot_names.each do |slot_name|
27
32
  # Ensure slot_name is not already declared
28
33
  if self.slots.key?(slot_name)
@@ -60,7 +60,7 @@ module ViewComponent
60
60
  # helper method with the same name as the slot.
61
61
  #
62
62
  # <%= render_inline(MyComponent.new) do |component| %>
63
- # <%= component.header(classes: "Foo") do %>
63
+ # <% component.header(classes: "Foo") do %>
64
64
  # <p>Bar</p>
65
65
  # <% end %>
66
66
  # <% end %>
@@ -95,7 +95,7 @@ module ViewComponent
95
95
  # helper method with the same name as the slot.
96
96
  #
97
97
  # <h1>
98
- # <%= items.each do |item| %>
98
+ # <% items.each do |item| %>
99
99
  # <%= item %>
100
100
  # <% end %>
101
101
  # </h1>
@@ -107,11 +107,11 @@ module ViewComponent
107
107
  # called multiple times to append to the slot.
108
108
  #
109
109
  # <%= render_inline(MyComponent.new) do |component| %>
110
- # <%= component.item(name: "Foo") do %>
110
+ # <% component.item(name: "Foo") do %>
111
111
  # <p>One</p>
112
112
  # <% end %>
113
113
  #
114
- # <%= component.item(name: "Bar") do %>
114
+ # <% component.item(name: "Bar") do %>
115
115
  # <p>two</p>
116
116
  # <% end %>
117
117
  # <% end %>
@@ -175,6 +175,10 @@ module ViewComponent
175
175
  end
176
176
 
177
177
  def validate_slot_name(slot_name)
178
+ if slot_name.to_sym == :content
179
+ raise ArgumentError.new("#{slot_name} is not a valid slot name.")
180
+ end
181
+
178
182
  if self.registered_slots.key?(slot_name)
179
183
  # TODO remove? This breaks overriding slots when slots are inherited
180
184
  raise ArgumentError.new("#{slot_name} slot declared multiple times")
@@ -227,7 +231,9 @@ module ViewComponent
227
231
  # current component. This is necessary to allow the lambda to access helper
228
232
  # methods like `content_tag` as well as parent component state.
229
233
  renderable_value = if block_given?
230
- slot_definition[:renderable_function].bind(self).call(*args, **kwargs) { view_context.capture(&block) }
234
+ slot_definition[:renderable_function].bind(self).call(*args, **kwargs) do |*args, **kwargs|
235
+ view_context.capture(*args, **kwargs, &block)
236
+ end
231
237
  else
232
238
  slot_definition[:renderable_function].bind(self).call(*args, **kwargs)
233
239
  end
@@ -249,7 +255,7 @@ module ViewComponent
249
255
  @_set_slots[slot_name] = slot
250
256
  end
251
257
 
252
- nil
258
+ slot
253
259
  end
254
260
  end
255
261
  end
@@ -39,7 +39,12 @@ module ViewComponent
39
39
  end
40
40
 
41
41
  def request
42
- @request ||= ActionDispatch::TestRequest.create
42
+ @request ||=
43
+ begin
44
+ request = ActionDispatch::TestRequest.create
45
+ request.session = ActionController::TestSession.new
46
+ request
47
+ end
43
48
  end
44
49
 
45
50
  def with_variant(variant)
@@ -60,6 +65,17 @@ module ViewComponent
60
65
  @controller = old_controller
61
66
  end
62
67
 
68
+ def with_request_url(path)
69
+ old_request_path_parameters = request.path_parameters
70
+ old_controller = defined?(@controller) && @controller
71
+
72
+ request.path_parameters = Rails.application.routes.recognize_path(path)
73
+ yield
74
+ ensure
75
+ request.path_parameters = old_request_path_parameters
76
+ @controller = old_controller
77
+ end
78
+
63
79
  def build_controller(klass)
64
80
  klass.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
65
81
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "i18n"
5
+ require "action_view/helpers/translation_helper"
6
+ require "active_support/concern"
7
+
8
+ module ViewComponent
9
+ module Translatable
10
+ extend ActiveSupport::Concern
11
+
12
+ HTML_SAFE_TRANSLATION_KEY = /(?:_|\b)html\z/.freeze
13
+
14
+ included do
15
+ class_attribute :i18n_backend, instance_writer: false, instance_predicate: false
16
+ end
17
+
18
+ class_methods do
19
+ def i18n_scope
20
+ @i18n_scope ||= virtual_path.sub(%r{^/}, "").gsub(%r{/_?}, ".")
21
+ end
22
+
23
+ def _after_compile
24
+ super
25
+
26
+ return if CompileCache.compiled? self
27
+
28
+ if (translation_files = _sidecar_files(%w[yml yaml])).any?
29
+ self.i18n_backend = I18nBackend.new(
30
+ i18n_scope: i18n_scope,
31
+ load_paths: translation_files,
32
+ )
33
+ else
34
+ # Cleanup if translations file has been removed since the last compilation
35
+ self.i18n_backend = nil
36
+ end
37
+ end
38
+ end
39
+
40
+ class I18nBackend < ::I18n::Backend::Simple
41
+ EMPTY_HASH = {}.freeze
42
+
43
+ def initialize(i18n_scope:, load_paths:)
44
+ @i18n_scope = i18n_scope.split(".")
45
+ @load_paths = load_paths
46
+ end
47
+
48
+ # Ensure the Simple backend won't load paths from ::I18n.load_path
49
+ def load_translations
50
+ super(@load_paths)
51
+ end
52
+
53
+ def scope_data(data)
54
+ @i18n_scope.reverse_each do |part|
55
+ data = { part => data}
56
+ end
57
+ data
58
+ end
59
+
60
+ def store_translations(locale, data, options = EMPTY_HASH)
61
+ super(locale, scope_data(data), options)
62
+ end
63
+ end
64
+
65
+ def translate(key = nil, **options)
66
+ return super unless i18n_backend
67
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
68
+
69
+ locale = options.delete(:locale) || ::I18n.locale
70
+ key = key&.to_s unless key.is_a?(String)
71
+ key = "#{i18n_scope}#{key}" if key.start_with?(".")
72
+
73
+ translated = catch(:exception) do
74
+ i18n_backend.translate(locale, key, options)
75
+ end
76
+
77
+ # Fallback to the global translations
78
+ if translated.is_a? ::I18n::MissingTranslation
79
+ return super(key, locale: locale, **options)
80
+ end
81
+
82
+ if HTML_SAFE_TRANSLATION_KEY.match?(key)
83
+ translated = translated.html_safe
84
+ end
85
+
86
+ translated
87
+ end
88
+ alias :t :translate
89
+
90
+ # Exposes .i18n_scope as an instance method
91
+ def i18n_scope
92
+ self.class.i18n_scope
93
+ end
94
+ end
95
+ end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 28
6
+ MINOR = 32
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module WithContentHelper
5
+ def with_content(value)
6
+ if value.nil?
7
+ raise ArgumentError.new("No content provided.")
8
+ else
9
+ @_content_set_by_with_content = value
10
+ end
11
+
12
+ self
13
+ end
14
+ end
15
+ 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.28.0
4
+ version: 2.32.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: 2021-03-16 00:00:00.000000000 Z
11
+ date: 2021-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -128,6 +128,20 @@ dependencies:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
130
  version: '1'
131
+ - !ruby/object:Gem::Dependency
132
+ name: jbuilder
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '2'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '2'
131
145
  - !ruby/object:Gem::Dependency
132
146
  name: rubocop
133
147
  requirement: !ruby/object:Gem::Requirement
@@ -198,7 +212,21 @@ dependencies:
198
212
  - - "~>"
199
213
  - !ruby/object:Gem::Version
200
214
  version: '0.13'
201
- description:
215
+ - !ruby/object:Gem::Dependency
216
+ name: yard
217
+ requirement: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: 0.9.25
222
+ type: :development
223
+ prerelease: false
224
+ version_requirements: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - "~>"
227
+ - !ruby/object:Gem::Version
228
+ version: 0.9.25
229
+ description:
202
230
  email:
203
231
  - opensource+view_component@github.com
204
232
  executables: []
@@ -232,6 +260,7 @@ files:
232
260
  - lib/view_component/collection.rb
233
261
  - lib/view_component/compile_cache.rb
234
262
  - lib/view_component/compiler.rb
263
+ - lib/view_component/component_error.rb
235
264
  - lib/view_component/engine.rb
236
265
  - lib/view_component/preview.rb
237
266
  - lib/view_component/preview_template_error.rb
@@ -249,13 +278,15 @@ files:
249
278
  - lib/view_component/template_error.rb
250
279
  - lib/view_component/test_case.rb
251
280
  - lib/view_component/test_helpers.rb
281
+ - lib/view_component/translatable.rb
252
282
  - lib/view_component/version.rb
283
+ - lib/view_component/with_content_helper.rb
253
284
  homepage: https://github.com/github/view_component
254
285
  licenses:
255
286
  - MIT
256
287
  metadata:
257
288
  allowed_push_host: https://rubygems.org
258
- post_install_message:
289
+ post_install_message:
259
290
  rdoc_options: []
260
291
  require_paths:
261
292
  - lib
@@ -270,8 +301,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
270
301
  - !ruby/object:Gem::Version
271
302
  version: '0'
272
303
  requirements: []
273
- rubygems_version: 3.1.2
274
- signing_key:
304
+ rubygems_version: 3.2.3
305
+ signing_key:
275
306
  specification_version: 4
276
307
  summary: View components for Rails
277
308
  test_files: []