view_component 2.26.1 → 2.31.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: 071c4caebc8e0dd567d8850adff146e65f6c8c1fe600b866f072d9bfa94bbc46
4
- data.tar.gz: 64df3c2eb8d27431da23a2c628de6c9c00d7988130494d37319f4a01d653fd9c
3
+ metadata.gz: 37bb719bf3f89d9d71ce7879e12a55ccafb0b1705565d4256a0f825b68a24db2
4
+ data.tar.gz: 5a379783bcf6f170e151fd48f1524e942710b3dfbc5cf40e790812476f303ae0
5
5
  SHA512:
6
- metadata.gz: 69bc43697d70dfb9b96280f47b80c06d1020fd180c01cbc6ae8c8416c3e1b97ffc6587d00741fa81743ae482a316e653790db5af2584c16baf3839a58b809195
7
- data.tar.gz: 636d5d91edb07cb90f44e3fc21e7ee63c3c115d1f5fbf7666619205b628761028c4963c65b30a181f27fbbd0bf9539cab0ec3271a48b4d747d7a705528218f11
6
+ metadata.gz: d0c18928b2cb3e6ae1e9a360eaeb05d278fee96c745bc4b17a0f498787862fb5619bf14165c37489d7d96bd064683fcd614166da3ee0c274453db0b47c270db8
7
+ data.tar.gz: 5381cd03176cfa7b9df47a86d801d7177c292c0f4460c8df70378e97052993f2bf17c9f96f140290d22d44a3a56794f4e9d9ac1378cb8c9666509d3f2fddac62
data/CHANGELOG.md CHANGED
@@ -2,6 +2,91 @@
2
2
 
3
3
  ## main
4
4
 
5
+ ## 2.31.0
6
+
7
+ * Add `#with_content` to allow setting content without a block.
8
+
9
+ *Jordan Raine, Manuel Puyol*
10
+
11
+ * Add `with_request_url` test helper.
12
+
13
+ *Mario Schüttel*
14
+
15
+ * Improve feature parity with Rails translations
16
+ * Don't create a translation backend if the component has no translation file
17
+ * Mark translation keys ending with `html` as HTML-safe
18
+ * Always convert keys to String
19
+ * Support multiple keys
20
+
21
+ *Elia Schito*
22
+
23
+ * Fix errors on `asset_url` helpers when `asset_host` has no protocol.
24
+
25
+ *Elia Schito*
26
+
27
+ * Prevent slots from overriding the `#content` method when registering a slot with that name.
28
+
29
+ *Blake Williams*
30
+
31
+ * Deprecate `with_slot` in favor of the new [slots API](https://viewcomponent.org/guide/slots.html).
32
+
33
+ *Manuel Puyol*
34
+
35
+ ## 2.30.0
36
+
37
+ * Deprecate `with_content_areas` in favor of [slots](https://viewcomponent.org/guide/slots.html).
38
+
39
+ *Joel Hawksley*
40
+
41
+ ## 2.29.0
42
+
43
+ * Allow Slot lambdas to share data from the parent component and allow chaining on the returned component.
44
+
45
+ *Sjors Baltus, Blake Williams*
46
+
47
+ * Experimental: Add `ViewComponent::Translatable`
48
+ * `t` and `translate` now will look first into the sidecar YAML translations file.
49
+ * `helpers.t` and `I18n.t` still reference the global Rails translation files.
50
+ * `l` and `localize` will still reference the global Rails translation files.
51
+
52
+ *Elia Schito*
53
+
54
+ * Fix rendering output of pass through slots when using HAML.
55
+
56
+ *Alex Robbin, Blake Williams*
57
+
58
+ * Experimental: call `._sidecar_files` to fetch the sidecar files for a given list of extensions, e.g. passing `["yml", "yaml"]`.
59
+
60
+ *Elia Schito*
61
+
62
+ * Fix bug where a single `jbuilder` template matched multiple template handlers.
63
+
64
+ *Niels Slot*
65
+
66
+ ## 2.28.0
67
+
68
+ * Include SlotableV2 by default in Base. **Note:** It's no longer necessary to include `ViewComponent::SlotableV2` to use Slots.
69
+
70
+ *Joel Hawksley*
71
+
72
+ * Prepend Preview routes instead of appending, accounting for cases where host application has catchall route.
73
+
74
+ *Joel Hawksley*
75
+
76
+ * Fix bug where blocks passed to lambda slots will render incorrectly in certain situations.
77
+
78
+ *Blake Williams*
79
+
80
+ ## 2.27.0
81
+
82
+ * Allow customization of the controller used in component tests.
83
+
84
+ *Alex Robbin*
85
+
86
+ * Generate preview at overridden path if one exists when using `--preview` flag.
87
+
88
+ *Nishiki Liu*
89
+
5
90
  ## 2.26.1
6
91
 
7
92
  * Fix bug that raises when trying to use a collection before the component has been compiled.
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.
@@ -8,7 +8,11 @@ module Preview
8
8
  check_class_collision suffix: "ComponentPreview"
9
9
 
10
10
  def create_preview_file
11
- template "component_preview.rb", File.join("test/components/previews", class_path, "#{file_name}_component_preview.rb")
11
+ preview_paths = Rails.application.config.view_component.preview_paths
12
+ return if preview_paths.count > 1
13
+
14
+ path_prefix = preview_paths.one? ? preview_paths.first : "test/components/previews"
15
+ template "component_preview.rb", File.join(path_prefix, class_path, "#{file_name}_component_preview.rb")
12
16
  end
13
17
 
14
18
  private
@@ -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,11 +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
16
+ include ViewComponent::SlotableV2
17
+ include ViewComponent::WithContentHelper
15
18
 
16
19
  ViewContextCalledBeforeRenderError = Class.new(StandardError)
17
20
 
@@ -78,6 +81,8 @@ module ViewComponent
78
81
  old_current_template = @current_template
79
82
  @current_template = self
80
83
 
84
+ raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if block && defined?(@_content_set_by_with_content)
85
+
81
86
  @_content_evaluated = false
82
87
  @_render_in_block = block
83
88
 
@@ -130,7 +135,7 @@ module ViewComponent
130
135
  @helpers ||= controller.view_context
131
136
  end
132
137
 
133
- # Exposes .virutal_path as an instance method
138
+ # Exposes .virtual_path as an instance method
134
139
  def virtual_path
135
140
  self.class.virtual_path
136
141
  end
@@ -168,8 +173,6 @@ module ViewComponent
168
173
  self
169
174
  end
170
175
 
171
- private
172
-
173
176
  # Exposes the current request to the component.
174
177
  # Use sparingly as doing so introduces coupling
175
178
  # that inhibits encapsulation & reuse.
@@ -177,6 +180,8 @@ module ViewComponent
177
180
  @request ||= controller.request
178
181
  end
179
182
 
183
+ private
184
+
180
185
  attr_reader :view_context
181
186
 
182
187
  def content
@@ -185,6 +190,8 @@ module ViewComponent
185
190
 
186
191
  @_content = if @view_context && @_render_in_block
187
192
  view_context.capture(self, &@_render_in_block)
193
+ elsif defined?(@_content_set_by_with_content)
194
+ @_content_set_by_with_content
188
195
  end
189
196
  end
190
197
 
@@ -193,8 +200,9 @@ module ViewComponent
193
200
  end
194
201
 
195
202
  # The controller used for testing components.
196
- # Defaults to ApplicationController. This should be set early
197
- # in the initialization process and should be set to a string.
203
+ # Defaults to ApplicationController, but can be configured
204
+ # on a per-test basis using `with_controller_class`.
205
+ # This should be set early in the initialization process and should be a string.
198
206
  mattr_accessor :test_controller
199
207
  @@test_controller = "ApplicationController"
200
208
 
@@ -204,6 +212,47 @@ module ViewComponent
204
212
  class << self
205
213
  attr_accessor :source_location, :virtual_path
206
214
 
215
+ # EXPERIMENTAL: This API is experimental and may be removed at any time.
216
+ # Find sidecar files for the given extensions.
217
+ #
218
+ # The provided array of extensions is expected to contain
219
+ # strings starting without the "dot", example: `["erb", "haml"]`.
220
+ #
221
+ # For example, one might collect sidecar CSS files that need to be compiled.
222
+ def _sidecar_files(extensions)
223
+ return [] unless source_location
224
+
225
+ extensions = extensions.join(",")
226
+
227
+ # view files in a directory named like the component
228
+ directory = File.dirname(source_location)
229
+ filename = File.basename(source_location, ".rb")
230
+ component_name = name.demodulize.underscore
231
+
232
+ # Add support for nested components defined in the same file.
233
+ #
234
+ # e.g.
235
+ #
236
+ # class MyComponent < ViewComponent::Base
237
+ # class MyOtherComponent < ViewComponent::Base
238
+ # end
239
+ # end
240
+ #
241
+ # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
242
+ nested_component_files = if name.include?("::") && component_name != filename
243
+ Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
244
+ else
245
+ []
246
+ end
247
+
248
+ # view files in the same directory as the component
249
+ sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
250
+
251
+ sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
252
+
253
+ (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
254
+ end
255
+
207
256
  # Render a component collection.
208
257
  def with_collection(collection, **args)
209
258
  Collection.new(self, collection, **args)
@@ -237,7 +286,7 @@ module ViewComponent
237
286
  end
238
287
 
239
288
  def compiled?
240
- template_compiler.compiled?
289
+ compiler.compiled?
241
290
  end
242
291
 
243
292
  # Compile templates to instance methods, assuming they haven't been compiled already.
@@ -245,11 +294,11 @@ module ViewComponent
245
294
  # Do as much work as possible in this step, as doing so reduces the amount
246
295
  # of work done each time a component is rendered.
247
296
  def compile(raise_errors: false)
248
- template_compiler.compile(raise_errors: raise_errors)
297
+ compiler.compile(raise_errors: raise_errors)
249
298
  end
250
299
 
251
- def template_compiler
252
- @_template_compiler ||= Compiler.new(self)
300
+ def compiler
301
+ @_compiler ||= Compiler.new(self)
253
302
  end
254
303
 
255
304
  # we'll eventually want to update this to support other types
@@ -266,6 +315,11 @@ module ViewComponent
266
315
  end
267
316
 
268
317
  def with_content_areas(*areas)
318
+ ActiveSupport::Deprecation.warn(
319
+ "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
320
+ "Use slots (https://viewcomponent.org/guide/slots.html) instead."
321
+ )
322
+
269
323
  if areas.include?(:content)
270
324
  raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
271
325
  end
@@ -317,7 +371,7 @@ module ViewComponent
317
371
  def validate_initialization_parameters!
318
372
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
319
373
 
320
- raise ArgumentError.new(
374
+ raise ViewComponent::ComponentError.new(
321
375
  "#{self} initializer cannot contain " \
322
376
  "`#{RESERVED_PARAMETER}` since it will override a " \
323
377
  "public ViewComponent method."
@@ -337,7 +391,7 @@ module ViewComponent
337
391
  end
338
392
 
339
393
  def counter_argument_present?
340
- instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
394
+ initialize_parameter_names.include?(collection_counter_parameter)
341
395
  end
342
396
 
343
397
  private
@@ -13,17 +13,23 @@ module ViewComponent
13
13
  def compile(raise_errors: false)
14
14
  return if compiled?
15
15
 
16
- if template_errors.present?
17
- raise ViewComponent::TemplateError.new(template_errors) if raise_errors
18
- return false
19
- end
16
+ subclass_instance_methods = component_class.instance_methods(false)
20
17
 
21
- if component_class.instance_methods(false).include?(:before_render_check)
18
+ if subclass_instance_methods.include?(:before_render_check)
22
19
  ActiveSupport::Deprecation.warn(
23
20
  "`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
24
21
  )
25
22
  end
26
23
 
24
+ if subclass_instance_methods.include?(:with_content) && raise_errors
25
+ raise ViewComponent::ComponentError.new("#{component_class} implements a reserved method, `with_content`.")
26
+ end
27
+
28
+ if template_errors.present?
29
+ raise ViewComponent::TemplateError.new(template_errors) if raise_errors
30
+ return false
31
+ end
32
+
27
33
  if raise_errors
28
34
  component_class.validate_initialization_parameters!
29
35
  component_class.validate_collection_parameter!
@@ -61,7 +67,7 @@ module ViewComponent
61
67
  "elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
62
68
  end.join("\n")
63
69
 
64
- component_class.class_eval <<-RUBY
70
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
65
71
  def render_template_for(variant = nil)
66
72
  if variant.nil?
67
73
  call
@@ -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
@@ -90,7 +90,7 @@ module ViewComponent
90
90
  options = app.config.view_component
91
91
 
92
92
  if options.show_previews
93
- app.routes.append do
93
+ app.routes.prepend do
94
94
  preview_controller = options.preview_controller.sub(/Controller$/, "").underscore
95
95
 
96
96
  get options.preview_route, to: "#{preview_controller}#index", as: :preview_view_components, internal: true
@@ -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)
@@ -49,14 +54,14 @@ module ViewComponent
49
54
 
50
55
  # If the slot is a collection, define an accesor that defaults to an empty array
51
56
  if collection
52
- class_eval <<-RUBY
57
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
53
58
  def #{accessor_name}
54
59
  content unless content_evaluated? # ensure content is loaded so slots will be defined
55
60
  #{instance_variable_name} ||= []
56
61
  end
57
62
  RUBY
58
63
  else
59
- class_eval <<-RUBY
64
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
60
65
  def #{accessor_name}
61
66
  content unless content_evaluated? # ensure content is loaded so slots will be defined
62
67
  #{instance_variable_name} if defined?(#{instance_variable_name})
@@ -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")
@@ -226,7 +230,13 @@ module ViewComponent
226
230
  # Use `bind(self)` to ensure lambda is executed in the context of the
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
- renderable_value = slot_definition[:renderable_function].bind(self).call(*args, **kwargs, &block)
233
+ renderable_value = if block_given?
234
+ slot_definition[:renderable_function].bind(self).call(*args, **kwargs) do |*args, **kwargs|
235
+ view_context.capture(*args, **kwargs, &block)
236
+ end
237
+ else
238
+ slot_definition[:renderable_function].bind(self).call(*args, **kwargs)
239
+ end
230
240
 
231
241
  # Function calls can return components, so if it's a component handle it specially
232
242
  if renderable_value.respond_to?(:render_in)
@@ -245,7 +255,7 @@ module ViewComponent
245
255
  @_set_slots[slot_name] = slot
246
256
  end
247
257
 
248
- nil
258
+ slot
249
259
  end
250
260
  end
251
261
  end
@@ -35,7 +35,7 @@ module ViewComponent
35
35
  end
36
36
 
37
37
  def controller
38
- @controller ||= Base.test_controller.constantize.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
38
+ @controller ||= build_controller(Base.test_controller.constantize)
39
39
  end
40
40
 
41
41
  def request
@@ -47,7 +47,32 @@ module ViewComponent
47
47
 
48
48
  controller.view_context.lookup_context.variants = variant
49
49
  yield
50
+ ensure
50
51
  controller.view_context.lookup_context.variants = old_variants
51
52
  end
53
+
54
+ def with_controller_class(klass)
55
+ old_controller = defined?(@controller) && @controller
56
+
57
+ @controller = build_controller(klass)
58
+ yield
59
+ ensure
60
+ @controller = old_controller
61
+ end
62
+
63
+ def with_request_url(path)
64
+ old_request_path_parameters = request.path_parameters
65
+ old_controller = defined?(@controller) && @controller
66
+
67
+ request.path_parameters = Rails.application.routes.recognize_path(path)
68
+ yield
69
+ ensure
70
+ request.path_parameters = old_request_path_parameters
71
+ @controller = old_controller
72
+ end
73
+
74
+ def build_controller(klass)
75
+ klass.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
76
+ end
52
77
  end
53
78
  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,9 +3,11 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 26
7
- PATCH = 1
6
+ MINOR = 31
7
+ PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
10
10
  end
11
11
  end
12
+
13
+ puts ViewComponent::VERSION::STRING if __FILE__ == $PROGRAM_NAME
@@ -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.26.1
4
+ version: 2.31.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: 2021-02-23 00:00:00.000000000 Z
11
+ date: 2021-04-26 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
@@ -232,6 +246,7 @@ files:
232
246
  - lib/view_component/collection.rb
233
247
  - lib/view_component/compile_cache.rb
234
248
  - lib/view_component/compiler.rb
249
+ - lib/view_component/component_error.rb
235
250
  - lib/view_component/engine.rb
236
251
  - lib/view_component/preview.rb
237
252
  - lib/view_component/preview_template_error.rb
@@ -249,7 +264,9 @@ files:
249
264
  - lib/view_component/template_error.rb
250
265
  - lib/view_component/test_case.rb
251
266
  - lib/view_component/test_helpers.rb
267
+ - lib/view_component/translatable.rb
252
268
  - lib/view_component/version.rb
269
+ - lib/view_component/with_content_helper.rb
253
270
  homepage: https://github.com/github/view_component
254
271
  licenses:
255
272
  - MIT
@@ -270,7 +287,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
270
287
  - !ruby/object:Gem::Version
271
288
  version: '0'
272
289
  requirements: []
273
- rubygems_version: 3.0.3
290
+ rubygems_version: 3.1.2
274
291
  signing_key:
275
292
  specification_version: 4
276
293
  summary: View components for Rails