view_component 2.26.1 → 2.31.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: 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