view_component 2.48.0 → 2.50.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: 3abce9e08dcb71614178ed68b9c8443211ff78790911057752d8fda6fbb194b7
4
- data.tar.gz: 3ac0a8ff1d7cafd2eb12e04a24f1d8f3362675833e4fda780bc238b8fc5ed41f
3
+ metadata.gz: 4ea90ad2ed037528865b7e6cf8b5850df938b624883a1508d85de52d8978c4fb
4
+ data.tar.gz: f2c1e558a700b9f79b3939785f3ff6c2f812a6e03b697f9b60f4aa6f6eaf54a0
5
5
  SHA512:
6
- metadata.gz: dba31d0d37e73b7a1e26863d167f5d1eaefb8c5f53d9f542e47667090802b5f53b5c6b9c4b0a9188bf76f42d375c5d7a2cd3024065620aa9ff40b15911706cb8
7
- data.tar.gz: f329872b25f36fe8fa0d018c2a5847ed0b6e47b095d16a27e89d2b0c7180f938d09730d0868405483a8e7c4fe4075afed56927a018b7a273ac4c94950d174fac
6
+ metadata.gz: 9fd97f786e4382098f81e5ec6230b6ae550991641f83d74ecfd67d3eacea515cd899ab21264c8ac5b5de1caed7f35304eb9b6cba13f092e5abdb3a6dc45f6b06
7
+ data.tar.gz: b70d4f308f47c6937d8aa2e518564ebbc6313366168e6382f4c296823ed07489c7be333f8f47822e65d3a5159753b5d3934ead8cd4323c3777d0c22dde9870f8
@@ -27,8 +27,8 @@ module PreviewHelper
27
27
  end.flatten
28
28
 
29
29
  # Search for templates the contain `html`.
30
- matching_templates = all_template_paths.find_all do |template|
31
- template =~ /#{template_identifier}*.(html)/
30
+ matching_templates = all_template_paths.find_all do |path|
31
+ path =~ /#{template_identifier}*.(html)/
32
32
  end
33
33
 
34
34
  # In-case of a conflict due to multiple template files with
data/docs/CHANGELOG.md CHANGED
@@ -7,6 +7,94 @@ title: Changelog
7
7
 
8
8
  ## main
9
9
 
10
+ ## 2.50.0
11
+
12
+ * Add tests for `layout` usage when rendering via controller.
13
+
14
+ *Felipe Sateler*
15
+
16
+ * Support returning Arrays from i18n files, and support marking them as HTML-safe translations.
17
+
18
+ *foca*
19
+
20
+ * Add Cometeer and Framework to users list.
21
+
22
+ *Elia Schito*
23
+
24
+ * Update Microsoft Vale styles.
25
+
26
+ *Simon Fish*
27
+
28
+ * Fix example in testing guide for how to setup default Rails tests.
29
+
30
+ *Steven Hansen*
31
+
32
+ * Update benchmark script to render multiple components/partials instead of a single instance per-run.
33
+
34
+ *Blake Williams*
35
+
36
+ * Add predicate methods `#{slot_name}?` to slots.
37
+
38
+ *Hans Lemuet*
39
+
40
+ * Use a dedicated deprecation instance, silence it while testing.
41
+
42
+ *Max Beizer, Hans Lemuet, Elia Schito*
43
+
44
+ * Fix Ruby warnings.
45
+
46
+ *Hans Lemuet*
47
+
48
+ * Place all generator options under `config.generate` namespace.
49
+
50
+ *Simon Fish*
51
+
52
+ * Allow preview generator to use provided component attributes.
53
+ * Add config option `config.view_component.generate.preview` to enable project-wide preview generation.
54
+ * Ensure all generated `.rb` files include `# frozen_string_literal: true` statement.
55
+
56
+ *Bob Maerten*
57
+
58
+ * Add Shogun to users list.
59
+
60
+ *Bernie Chiu*
61
+
62
+ ## 2.49.1
63
+
64
+ * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
65
+
66
+ *Cameron Dutro*
67
+
68
+ ## 2.49.0
69
+
70
+ * Fix path handling for evaluated view components that broke in Ruby 3.1.
71
+
72
+ *Adam Hess*
73
+
74
+ * Fix support for the `default:` option for a global translation.
75
+
76
+ *Elia Schito*
77
+
78
+ * Ensure i18n scope is a symbol to protect lookups.
79
+
80
+ *Simon Fish*
81
+
82
+ * Small update to preview docs to include rspec mention.
83
+
84
+ *Leigh Halliday*
85
+
86
+ * Small improvements to collection iteration docs.
87
+
88
+ *Brian O'Rourke*
89
+
90
+ * Add good and bad examples to `ViewComponents in practice`.
91
+
92
+ *Joel Hawksley*
93
+
94
+ * Add Ruby 3.1 and Rails 7.0 to CI
95
+
96
+ *Peter Goldstein*
97
+
10
98
  ## 2.48.0
11
99
 
12
100
  * Correct path in example test command in Contributing docs.
@@ -583,6 +671,12 @@ title: Changelog
583
671
 
584
672
  *Joel Hawksley*
585
673
 
674
+ ## 2.31.2
675
+
676
+ * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
677
+
678
+ *Cameron Dutro*
679
+
586
680
  ## 2.31.1
587
681
 
588
682
  * Fix `DEPRECATION WARNING: before_render_check` when compiling `ViewComponent::Base`
@@ -44,7 +44,7 @@ module ViewComponent
44
44
  end
45
45
 
46
46
  def sidecar?
47
- options["sidecar"] || ViewComponent::Base.generate_sidecar
47
+ options["sidecar"] || ViewComponent::Base.generate.sidecar
48
48
  end
49
49
  end
50
50
  end
@@ -11,11 +11,13 @@ module Rails
11
11
 
12
12
  argument :attributes, type: :array, default: [], banner: "attribute"
13
13
  check_class_collision suffix: "Component"
14
+
14
15
  class_option :inline, type: :boolean, default: false
16
+ class_option :locale, type: :boolean, default: ViewComponent::Base.generate.locale
15
17
  class_option :parent, type: :string, desc: "The parent class for the generated component"
16
- class_option :stimulus, type: :boolean, default: ViewComponent::Base.generate_stimulus_controller
18
+ class_option :preview, type: :boolean, default: ViewComponent::Base.generate.preview
17
19
  class_option :sidecar, type: :boolean, default: false
18
- class_option :locale, type: :boolean, default: ViewComponent::Base.generate_locale
20
+ class_option :stimulus, type: :boolean, default: ViewComponent::Base.generate.stimulus_controller
19
21
 
20
22
  def create_component_file
21
23
  template "component.rb", File.join(component_path, class_path, "#{file_name}_component.rb")
@@ -12,7 +12,7 @@ module Locale
12
12
  class_option :sidecar, type: :boolean, default: false
13
13
 
14
14
  def create_locale_file
15
- if ViewComponent::Base.generate_distinct_locale_files
15
+ if ViewComponent::Base.generate.distinct_locale_files
16
16
  I18n.available_locales.each do |locale|
17
17
  create_file destination(locale), translations_hash([locale]).to_yaml
18
18
  end
@@ -5,6 +5,7 @@ module Preview
5
5
  class ComponentGenerator < ::Rails::Generators::NamedBase
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
+ argument :attributes, type: :array, default: [], banner: "attribute"
8
9
  check_class_collision suffix: "ComponentPreview"
9
10
 
10
11
  def create_preview_file
@@ -20,6 +21,12 @@ module Preview
20
21
  def file_name
21
22
  @_file_name ||= super.sub(/_component\z/i, "")
22
23
  end
24
+
25
+ def render_signature
26
+ return if attributes.blank?
27
+
28
+ attributes.map { |attr| %(#{attr.name}: "#{attr.name}") }.join(", ")
29
+ end
23
30
  end
24
31
  end
25
32
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class <%= class_name %>ComponentPreview < ViewComponent::Preview
2
4
  def default
3
- render(<%= class_name %>Component.new)
5
+ render(<%= class_name %>Component.new<%= "(#{render_signature})" if render_signature %>)
4
6
  end
5
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails_helper"
2
4
 
3
5
  RSpec.describe <%= class_name %>Component, type: :component do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "test_helper"
2
4
 
3
5
  class <%= class_name %>ComponentTest < ViewComponent::TestCase
@@ -40,6 +40,23 @@ module ViewComponent
40
40
  # noop
41
41
  end
42
42
 
43
+ # @!macro [attach] deprecated_generate_mattr_accessor
44
+ # @method generate_$1
45
+ # @deprecated Use `#generate.$1` instead. Will be removed in v3.0.0.
46
+ def self._deprecated_generate_mattr_accessor(name)
47
+ define_singleton_method("generate_#{name}".to_sym) do
48
+ generate.public_send(name)
49
+ end
50
+ define_singleton_method("generate_#{name}=".to_sym) do |value|
51
+ generate.public_send("#{name}=".to_sym, value)
52
+ end
53
+ end
54
+
55
+ _deprecated_generate_mattr_accessor :distinct_locale_files
56
+ _deprecated_generate_mattr_accessor :locale
57
+ _deprecated_generate_mattr_accessor :sidecar
58
+ _deprecated_generate_mattr_accessor :stimulus_controller
59
+
43
60
  # Entrypoint for rendering components.
44
61
  #
45
62
  # - `view_context`: ActionView context from calling view
@@ -218,14 +235,11 @@ module ViewComponent
218
235
  # @param variant [Symbol] The variant to be used by the component.
219
236
  # @return [self]
220
237
  def with_variant(variant)
221
- ActiveSupport::Deprecation.warn(
222
- "`with_variant` is deprecated and will be removed in ViewComponent v3.0.0."
223
- )
224
-
225
238
  @__vc_variant = variant
226
239
 
227
240
  self
228
241
  end
242
+ deprecate :with_variant, deprecator: ViewComponent::Deprecation
229
243
 
230
244
  # The current request. Use sparingly as doing so introduces coupling that
231
245
  # inhibits encapsulation & reuse, often making testing difficult.
@@ -271,56 +285,63 @@ module ViewComponent
271
285
  #
272
286
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
273
287
 
274
- # Always generate a Stimulus controller alongside the component:
288
+ # Path for component files
275
289
  #
276
- # config.view_component.generate_stimulus_controller = true
290
+ # config.view_component.view_component_path = "app/my_components"
277
291
  #
278
- # Defaults to `false`.
292
+ # Defaults to `app/components`.
279
293
  #
280
- mattr_accessor :generate_stimulus_controller, instance_writer: false, default: false
294
+ mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
281
295
 
282
- # Always generate translations file alongside the component:
296
+ # Parent class for generated components
283
297
  #
284
- # config.view_component.generate_locale = true
298
+ # config.view_component.component_parent_class = "MyBaseComponent"
285
299
  #
286
- # Defaults to `false`.
300
+ # Defaults to nil. If this is falsy, generators will use
301
+ # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
287
302
  #
288
- mattr_accessor :generate_locale, instance_writer: false, default: false
303
+ mattr_accessor :component_parent_class, instance_writer: false
289
304
 
290
- # Always generate as many translations files as available locales:
305
+ # Configuration for generators.
291
306
  #
292
- # config.view_component.generate_distinct_locale_files = true
307
+ # All options under this namespace default to `false` unless otherwise
308
+ # stated.
293
309
  #
294
- # Defaults to `false`.
310
+ # #### #sidecar
295
311
  #
296
- # One file will be generated for each configured `I18n.available_locales`.
297
- # Fallback on `[:en]` when no available_locales is defined.
312
+ # Always generate a component with a sidecar directory:
298
313
  #
299
- mattr_accessor :generate_distinct_locale_files, instance_writer: false, default: false
300
-
301
- # Path for component files
314
+ # config.view_component.generate.sidecar = true
302
315
  #
303
- # config.view_component.view_component_path = "app/my_components"
316
+ # #### #stimulus_controller
304
317
  #
305
- # Defaults to `app/components`.
318
+ # Always generate a Stimulus controller alongside the component:
306
319
  #
307
- mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
308
-
309
- # Parent class for generated components
320
+ # config.view_component.generate.stimulus_controller = true
310
321
  #
311
- # config.view_component.component_parent_class = "MyBaseComponent"
322
+ # #### #locale
323
+ #
324
+ # Always generate translations file alongside the component:
312
325
  #
313
- # Defaults to "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
326
+ # config.view_component.generate.locale = true
314
327
  #
315
- mattr_accessor :component_parent_class, instance_writer: false
316
-
317
- # Always generate a component with a sidecar directory:
328
+ # #### #distinct_locale_files
329
+ #
330
+ # Always generate as many translations files as available locales:
331
+ #
332
+ # config.view_component.generate.distinct_locale_files = true
333
+ #
334
+ # One file will be generated for each configured `I18n.available_locales`,
335
+ # falling back to `[:en]` when no `available_locales` is defined.
336
+ #
337
+ # #### #preview
318
338
  #
319
- # config.view_component.generate_sidecar = true
339
+ # Always generate preview alongside the component:
320
340
  #
321
- # Defaults to `false`.
341
+ # config.view_component.generate.preview = true
322
342
  #
323
- mattr_accessor :generate_sidecar, instance_writer: false, default: false
343
+ # Defaults to `false`.
344
+ mattr_accessor :generate, instance_writer: false, default: ActiveSupport::OrderedOptions.new(false)
324
345
 
325
346
  class << self
326
347
  # @private
@@ -401,7 +422,7 @@ module ViewComponent
401
422
  # Derive the source location of the component Ruby file from the call stack.
402
423
  # We need to ignore `inherited` frames here as they indicate that `inherited`
403
424
  # has been re-defined by the consuming application, likely in ApplicationComponent.
404
- child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
425
+ child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].path
405
426
 
406
427
  # Removes the first part of the path and the extension.
407
428
  child.virtual_path = child.source_location.gsub(
@@ -49,7 +49,7 @@ module ViewComponent
49
49
  end
50
50
 
51
51
  if subclass_instance_methods.include?(:before_render_check)
52
- ActiveSupport::Deprecation.warn(
52
+ ViewComponent::Deprecation.warn(
53
53
  "`#before_render_check` will be removed in v3.0.0.\n\n" \
54
54
  "To fix this issue, use `#before_render` instead."
55
55
  )
@@ -31,7 +31,7 @@ module ViewComponent
31
31
 
32
32
  class_methods do
33
33
  def with_content_areas(*areas)
34
- ActiveSupport::Deprecation.warn(
34
+ ViewComponent::Deprecation.warn(
35
35
  "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n\n" \
36
36
  "Use slots (https://viewcomponent.org/guide/slots.html) instead."
37
37
  )
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/deprecation"
4
+
5
+ module ViewComponent
6
+ DEPRECATION_HORIZON = 3
7
+ Deprecation = ActiveSupport::Deprecation.new(DEPRECATION_HORIZON.to_s, "ViewComponent")
8
+ end
@@ -0,0 +1,18 @@
1
+ ---
2
+ layout: default
3
+ title: API
4
+ nav_order: 3
5
+ ---
6
+
7
+ <!-- Warning: AUTO-GENERATED file, don't edit. Add code comments to your Ruby instead <3 -->
8
+
9
+ # API
10
+
11
+ <% @sections.each do |section| %>
12
+ ## <%= section.heading %>
13
+
14
+ <% section.methods.each do |method| %>
15
+ ### <%== render ViewComponent::DocsBuilderComponent::MethodDoc.new(method) %>
16
+
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ class DocsBuilderComponent < Base
5
+ class Section < Struct.new(:heading, :methods, :show_types, keyword_init: true)
6
+ def initialize(heading: nil, methods: [], show_types: true)
7
+ methods.sort_by! { |method| method[:name] }
8
+ super
9
+ end
10
+ end
11
+
12
+ class MethodDoc < ViewComponent::Base
13
+ def initialize(method, section: Section.new(show_types: true))
14
+ @method = method
15
+ @section = section
16
+ end
17
+
18
+ def show_types?
19
+ @section.show_types
20
+ end
21
+
22
+ def deprecated?
23
+ @method.tag(:deprecated).present?
24
+ end
25
+
26
+ def suffix
27
+ " (Deprecated)" if deprecated?
28
+ end
29
+
30
+ def types
31
+ " → [#{@method.tag(:return).types.join(',')}]" if @method.tag(:return)&.types && show_types?
32
+ end
33
+
34
+ def signature_or_name
35
+ @method.signature ? @method.signature.gsub("def ", "") : @method.name
36
+ end
37
+
38
+ def separator
39
+ @method.sep
40
+ end
41
+
42
+ def docstring
43
+ @method.docstring
44
+ end
45
+
46
+ def deprecation_text
47
+ @method.tag(:deprecated)&.text
48
+ end
49
+
50
+ def docstring_and_deprecation_text
51
+ <<~DOCS.strip
52
+ #{docstring}
53
+
54
+ #{"_#{deprecation_text}_" if deprecated?}
55
+ DOCS
56
+ end
57
+
58
+ def call
59
+ <<~DOCS.chomp
60
+ #{separator}#{signature_or_name}#{types}#{suffix}
61
+
62
+ #{docstring_and_deprecation_text}
63
+ DOCS
64
+ end
65
+ end
66
+
67
+ # { heading: String, public_only: Boolean, show_types: Boolean}
68
+ def initialize(sections: [])
69
+ @sections = sections
70
+ end
71
+
72
+ # deprecation
73
+ # return
74
+ # only public methods
75
+ # sig with types or name
76
+ end
77
+ end
@@ -27,7 +27,7 @@ module ViewComponent
27
27
  )
28
28
 
29
29
  if options.preview_path.present?
30
- ActiveSupport::Deprecation.warn(
30
+ ViewComponent::Deprecation.warn(
31
31
  "`preview_path` will be removed in v3.0.0. Use `preview_paths` instead."
32
32
  )
33
33
  options.preview_paths << options.preview_path
@@ -155,7 +155,9 @@ end
155
155
 
156
156
  # :nocov:
157
157
  unless defined?(ViewComponent::Base)
158
- ActiveSupport::Deprecation.warn(
158
+ require "view_component/deprecation"
159
+
160
+ ViewComponent::Deprecation.warn(
159
161
  "This manually engine loading is deprecated and will be removed in v3.0.0. " \
160
162
  "Remove `require \"view_component/engine\"`."
161
163
  )
@@ -5,14 +5,16 @@ require "active_support/notifications"
5
5
  module ViewComponent # :nodoc:
6
6
  module Instrumentation
7
7
  def self.included(mod)
8
- mod.prepend(self)
8
+ mod.prepend(self) unless ancestors.include?(ViewComponent::Instrumentation)
9
9
  end
10
10
 
11
11
  def render_in(view_context, &block)
12
12
  ActiveSupport::Notifications.instrument(
13
13
  "!render.view_component",
14
- name: self.class.name,
15
- identifier: self.class.identifier
14
+ {
15
+ name: self.class.name,
16
+ identifier: self.class.identifier
17
+ }
16
18
  ) do
17
19
  super(view_context, &block)
18
20
  end
@@ -25,12 +25,19 @@ module ViewComponent
25
25
  end
26
26
 
27
27
  def register_polymorphic_slot(slot_name, types, collection:)
28
+ unless types.empty?
29
+ getter_name = slot_name
30
+
31
+ define_method(getter_name) do
32
+ get_slot(slot_name)
33
+ end
34
+ end
35
+
28
36
  renderable_hash = types.each_with_object({}) do |(poly_type, poly_callable), memo|
29
37
  memo[poly_type] = define_slot(
30
38
  "#{slot_name}_#{poly_type}", collection: collection, callable: poly_callable
31
39
  )
32
40
 
33
- getter_name = slot_name
34
41
  setter_name =
35
42
  if collection
36
43
  "#{ActiveSupport::Inflector.singularize(slot_name)}_#{poly_type}"
@@ -38,10 +45,6 @@ module ViewComponent
38
45
  "#{slot_name}_#{poly_type}"
39
46
  end
40
47
 
41
- define_method(getter_name) do
42
- get_slot(slot_name)
43
- end
44
-
45
48
  define_method(setter_name) do |*args, &block|
46
49
  set_polymorphic_slot(slot_name, poly_type, *args, &block)
47
50
  end
@@ -73,8 +73,8 @@ module ViewComponent # :nodoc:
73
73
  # Returns the relative path (from preview_path) to the preview example template if the template exists
74
74
  def preview_example_template_path(example)
75
75
  preview_path =
76
- Array(preview_paths).detect do |preview_path|
77
- Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
76
+ Array(preview_paths).detect do |path|
77
+ Dir["#{path}/#{preview_name}_preview/#{example}.html.*"].first
78
78
  end
79
79
 
80
80
  if preview_path.nil?
@@ -23,7 +23,7 @@ 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(
26
+ ViewComponent::Deprecation.warn(
27
27
  "`with_slot` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
28
28
  "Use the new slots API (https://viewcomponent.org/guide/slots.html) instead."
29
29
  )
@@ -7,6 +7,11 @@ module ViewComponent
7
7
  module SlotableV2
8
8
  extend ActiveSupport::Concern
9
9
 
10
+ RESERVED_NAMES = {
11
+ singular: %i[content render].freeze,
12
+ plural: %i[contents renders].freeze,
13
+ }.freeze
14
+
10
15
  # Setup component slot state
11
16
  included do
12
17
  # Hash of registered Slots
@@ -75,6 +80,10 @@ module ViewComponent
75
80
  end
76
81
  ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
77
82
 
83
+ define_method "#{slot_name}?" do
84
+ get_slot(slot_name).present?
85
+ end
86
+
78
87
  register_slot(slot_name, collection: false, callable: callable)
79
88
  end
80
89
 
@@ -140,6 +149,10 @@ module ViewComponent
140
149
  end
141
150
  end
142
151
 
152
+ define_method "#{slot_name}?" do
153
+ get_slot(slot_name).present?
154
+ end
155
+
143
156
  register_slot(slot_name, collection: true, callable: callable)
144
157
  end
145
158
 
@@ -197,24 +210,26 @@ module ViewComponent
197
210
  end
198
211
 
199
212
  def validate_plural_slot_name(slot_name)
200
- if slot_name.to_sym == :contents
213
+ if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
201
214
  raise ArgumentError.new(
202
215
  "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
203
216
  "To fix this issue, choose a different name."
204
217
  )
205
218
  end
206
219
 
220
+ raise_if_slot_ends_with_question_mark(slot_name)
207
221
  raise_if_slot_registered(slot_name)
208
222
  end
209
223
 
210
224
  def validate_singular_slot_name(slot_name)
211
- if slot_name.to_sym == :content
225
+ if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
212
226
  raise ArgumentError.new(
213
227
  "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
214
228
  "To fix this issue, choose a different name."
215
229
  )
216
230
  end
217
231
 
232
+ raise_if_slot_ends_with_question_mark(slot_name)
218
233
  raise_if_slot_registered(slot_name)
219
234
  end
220
235
 
@@ -227,6 +242,17 @@ module ViewComponent
227
242
  )
228
243
  end
229
244
  end
245
+
246
+ def raise_if_slot_ends_with_question_mark(slot_name)
247
+ if slot_name.to_s.ends_with?("?")
248
+ raise ArgumentError.new(
249
+ "#{self} declares a slot named #{slot_name}, which ends with a question mark.\n\n"\
250
+ "This is not allowed because the ViewComponent framework already provides predicate "\
251
+ "methods ending in `?`.\n\n" \
252
+ "To fix this issue, choose a different name."
253
+ )
254
+ end
255
+ end
230
256
  end
231
257
 
232
258
  def get_slot(slot_name)
@@ -276,8 +302,8 @@ module ViewComponent
276
302
  renderable_function = slot_definition[:renderable_function].bind(self)
277
303
  renderable_value =
278
304
  if block_given?
279
- renderable_function.call(*args) do |*args|
280
- view_context.capture(*args, &block)
305
+ renderable_function.call(*args) do |*rargs|
306
+ view_context.capture(*rargs, &block)
281
307
  end
282
308
  else
283
309
  renderable_function.call(*args)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "erb"
3
4
  require "set"
4
5
  require "i18n"
5
6
  require "action_view/helpers/translation_helper"
@@ -41,7 +42,7 @@ module ViewComponent
41
42
  EMPTY_HASH = {}.freeze
42
43
 
43
44
  def initialize(i18n_scope:, load_paths:)
44
- @i18n_scope = i18n_scope.split(".")
45
+ @i18n_scope = i18n_scope.split(".").map(&:to_sym)
45
46
  @load_paths = load_paths
46
47
  end
47
48
 
@@ -70,21 +71,29 @@ module ViewComponent
70
71
  key = key&.to_s unless key.is_a?(String)
71
72
  key = "#{i18n_scope}#{key}" if key.start_with?(".")
72
73
 
73
- translated =
74
- catch(:exception) do
75
- i18n_backend.translate(locale, key, options)
74
+ if HTML_SAFE_TRANSLATION_KEY.match?(key)
75
+ html_escape_translation_options!(options)
76
+ end
77
+
78
+ if key.start_with?(i18n_scope + ".")
79
+ translated =
80
+ catch(:exception) do
81
+ i18n_backend.translate(locale, key, options)
82
+ end
83
+
84
+ # Fallback to the global translations
85
+ if translated.is_a? ::I18n::MissingTranslation
86
+ return super(key, locale: locale, **options)
76
87
  end
77
88
 
78
- # Fallback to the global translations
79
- if translated.is_a? ::I18n::MissingTranslation
80
- return super(key, locale: locale, **options)
81
- end
89
+ if HTML_SAFE_TRANSLATION_KEY.match?(key)
90
+ translated = html_safe_translation(translated)
91
+ end
82
92
 
83
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
84
- translated = translated.html_safe # rubocop:disable Rails/OutputSafety
93
+ translated
94
+ else
95
+ super(key, locale: locale, **options)
85
96
  end
86
-
87
- translated
88
97
  end
89
98
  alias :t :translate
90
99
 
@@ -92,5 +101,30 @@ module ViewComponent
92
101
  def i18n_scope
93
102
  self.class.i18n_scope
94
103
  end
104
+
105
+ def html_safe_translation(translation)
106
+ if translation.respond_to?(:map)
107
+ translation.map { |element| html_safe_translation(element) }
108
+ else
109
+ # It's assumed here that objects loaded by the i18n backend will respond to `#html_safe?`.
110
+ # It's reasonable that if we're in Rails, `active_support/core_ext/string/output_safety.rb`
111
+ # will provide this to `Object`.
112
+ translation.html_safe # rubocop:disable Rails/OutputSafety
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def html_escape_translation_options!(options)
119
+ options.each do |name, value|
120
+ unless i18n_option?(name) || (name == :count && value.is_a?(Numeric))
121
+ options[name] = ERB::Util.html_escape(value.to_s)
122
+ end
123
+ end
124
+ end
125
+
126
+ def i18n_option?(name)
127
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
128
+ end
95
129
  end
96
130
  end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 48
6
+ MINOR = 50
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
@@ -10,6 +10,7 @@ module ViewComponent
10
10
  autoload :Compiler
11
11
  autoload :CompileCache
12
12
  autoload :ComponentError
13
+ autoload :Deprecation
13
14
  autoload :Instrumentation
14
15
  autoload :Preview
15
16
  autoload :PreviewTemplateError
@@ -21,8 +22,8 @@ end
21
22
 
22
23
  # :nocov:
23
24
  if defined?(ViewComponent::Engine)
24
- ActiveSupport::Deprecation.warn(
25
- "This manually engine loading is deprecated and will be removed in v3.0.0. " \
25
+ ViewComponent::Deprecation.warn(
26
+ "Manually loading the engine is deprecated and will be removed in v3.0.0. " \
26
27
  "Remove `require \"view_component/engine\"`."
27
28
  )
28
29
  elsif defined?(Rails::Engine)
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.48.0
4
+ version: 2.50.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: 2022-01-14 00:00:00.000000000 Z
11
+ date: 2022-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -329,6 +329,9 @@ files:
329
329
  - lib/view_component/compiler.rb
330
330
  - lib/view_component/component_error.rb
331
331
  - lib/view_component/content_areas.rb
332
+ - lib/view_component/deprecation.rb
333
+ - lib/view_component/docs_builder_component.html.erb
334
+ - lib/view_component/docs_builder_component.rb
332
335
  - lib/view_component/engine.rb
333
336
  - lib/view_component/instrumentation.rb
334
337
  - lib/view_component/polymorphic_slots.rb
@@ -373,7 +376,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
373
376
  - !ruby/object:Gem::Version
374
377
  version: '0'
375
378
  requirements: []
376
- rubygems_version: 3.1.4
379
+ rubygems_version: 3.2.32
377
380
  signing_key:
378
381
  specification_version: 4
379
382
  summary: View components for Rails