view_component 2.28.0 → 2.32.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: 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: []