view_component 2.49.0 → 2.51.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: 8feda954b97fc366da967bee72554a1975a40f50aef0cbd1cde6f83f97547d49
4
- data.tar.gz: 92312db0824d58d24ee8f74f56c3a14e961f03fc71bb1669b1a529fedfb225f3
3
+ metadata.gz: 163414625d8e2df6f2afddc821a6c849aef71cc8cd1ba458a533d90840e545cc
4
+ data.tar.gz: 3518269c96d4e62d3bcedae22871c7b260d2abf42e0c1e32703115185e69eeea
5
5
  SHA512:
6
- metadata.gz: 1ff56022e2212300d6ae5f8ea8700364fc5675354476b1830e05885c6f4beff3f7b75d6733531eea10aa1b0830917e025686bf17af89606ed2e457aaf1004f73
7
- data.tar.gz: 5d8dddd267a4a627af6ba0a07507d9d0c2dd2ae38da92f542a690e2fa3052bdc56aa37488e1fde440c9a064b825f0ef1db3ec838f06eba4a4598b4e8c95e315f
6
+ metadata.gz: 531785b833a4d1a83b6929095b6a6a34076cb6576820884487a247d70c37bde2fedc47914633674c59a888dc7270526660e121f4980f2bcb85625ed6009607c4
7
+ data.tar.gz: e4c8b2694c7f70c529d56dcced18f66d511e909e1f36abc604a58402d1332caecde59d40c065668454af8abf6d53578469539ac1f4366650ad286856b50776fa
@@ -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,82 @@ title: Changelog
7
7
 
8
8
  ## main
9
9
 
10
+ ## 2.51.0
11
+
12
+ * Update the docs only when releasing a new version.
13
+
14
+ *Hans Lemuet*
15
+
16
+ * Alphabetize companies using ViewComponent and add Brightline to the list.
17
+
18
+ *Jack Schuss*
19
+
20
+ * Add CMYK value for ViewComponent Red color on logo page.
21
+
22
+ *Dylan Smith*
23
+
24
+ * Improve performance by moving template compilation from `#render_in` to `#render_template_for`.
25
+
26
+ *Cameron Dutro*
27
+
28
+ ## 2.50.0
29
+
30
+ * Add tests for `layout` usage when rendering via controller.
31
+
32
+ *Felipe Sateler*
33
+
34
+ * Support returning Arrays from i18n files, and support marking them as HTML-safe translations.
35
+
36
+ *foca*
37
+
38
+ * Add Cometeer and Framework to users list.
39
+
40
+ *Elia Schito*
41
+
42
+ * Update Microsoft Vale styles.
43
+
44
+ *Simon Fish*
45
+
46
+ * Fix example in testing guide for how to setup default Rails tests.
47
+
48
+ *Steven Hansen*
49
+
50
+ * Update benchmark script to render multiple components/partials instead of a single instance per-run.
51
+
52
+ *Blake Williams*
53
+
54
+ * Add predicate methods `#{slot_name}?` to slots.
55
+
56
+ *Hans Lemuet*
57
+
58
+ * Use a dedicated deprecation instance, silence it while testing.
59
+
60
+ *Max Beizer, Hans Lemuet, Elia Schito*
61
+
62
+ * Fix Ruby warnings.
63
+
64
+ *Hans Lemuet*
65
+
66
+ * Place all generator options under `config.generate` namespace.
67
+
68
+ *Simon Fish*
69
+
70
+ * Allow preview generator to use provided component attributes.
71
+ * Add config option `config.view_component.generate.preview` to enable project-wide preview generation.
72
+ * Ensure all generated `.rb` files include `# frozen_string_literal: true` statement.
73
+
74
+ *Bob Maerten*
75
+
76
+ * Add Shogun to users list.
77
+
78
+ *Bernie Chiu*
79
+
80
+ ## 2.49.1
81
+
82
+ * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
83
+
84
+ *Cameron Dutro*
85
+
10
86
  ## 2.49.0
11
87
 
12
88
  * Fix path handling for evaluated view components that broke in Ruby 3.1.
@@ -613,6 +689,12 @@ title: Changelog
613
689
 
614
690
  *Joel Hawksley*
615
691
 
692
+ ## 2.31.2
693
+
694
+ * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
695
+
696
+ *Cameron Dutro*
697
+
616
698
  ## 2.31.1
617
699
 
618
700
  * 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
@@ -49,8 +66,6 @@ module ViewComponent
49
66
  #
50
67
  # @return [String]
51
68
  def render_in(view_context, &block)
52
- self.class.compile(raise_errors: true)
53
-
54
69
  @view_context = view_context
55
70
  self.__vc_original_view_context ||= view_context
56
71
 
@@ -95,6 +110,16 @@ module ViewComponent
95
110
  @current_template = old_current_template
96
111
  end
97
112
 
113
+ # :nocov:
114
+ def render_template_for(variant = nil)
115
+ # Force compilation here so the compiler always redefines render_template_for.
116
+ # This is mostly a safeguard to prevent infinite recursion.
117
+ self.class.compile(raise_errors: true, force: true)
118
+ # .compile replaces this method; call the new one
119
+ render_template_for(variant)
120
+ end
121
+ # :nocov:
122
+
98
123
  # EXPERIMENTAL: Optional content to be returned after the rendered template.
99
124
  #
100
125
  # @return [String]
@@ -218,14 +243,11 @@ module ViewComponent
218
243
  # @param variant [Symbol] The variant to be used by the component.
219
244
  # @return [self]
220
245
  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
246
  @__vc_variant = variant
226
247
 
227
248
  self
228
249
  end
250
+ deprecate :with_variant, deprecator: ViewComponent::Deprecation
229
251
 
230
252
  # The current request. Use sparingly as doing so introduces coupling that
231
253
  # inhibits encapsulation & reuse, often making testing difficult.
@@ -271,56 +293,63 @@ module ViewComponent
271
293
  #
272
294
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
273
295
 
274
- # Always generate a Stimulus controller alongside the component:
296
+ # Path for component files
275
297
  #
276
- # config.view_component.generate_stimulus_controller = true
298
+ # config.view_component.view_component_path = "app/my_components"
277
299
  #
278
- # Defaults to `false`.
300
+ # Defaults to `app/components`.
279
301
  #
280
- mattr_accessor :generate_stimulus_controller, instance_writer: false, default: false
302
+ mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
281
303
 
282
- # Always generate translations file alongside the component:
304
+ # Parent class for generated components
283
305
  #
284
- # config.view_component.generate_locale = true
306
+ # config.view_component.component_parent_class = "MyBaseComponent"
285
307
  #
286
- # Defaults to `false`.
308
+ # Defaults to nil. If this is falsy, generators will use
309
+ # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
287
310
  #
288
- mattr_accessor :generate_locale, instance_writer: false, default: false
311
+ mattr_accessor :component_parent_class, instance_writer: false
289
312
 
290
- # Always generate as many translations files as available locales:
313
+ # Configuration for generators.
291
314
  #
292
- # config.view_component.generate_distinct_locale_files = true
315
+ # All options under this namespace default to `false` unless otherwise
316
+ # stated.
293
317
  #
294
- # Defaults to `false`.
318
+ # #### #sidecar
295
319
  #
296
- # One file will be generated for each configured `I18n.available_locales`.
297
- # Fallback on `[:en]` when no available_locales is defined.
320
+ # Always generate a component with a sidecar directory:
298
321
  #
299
- mattr_accessor :generate_distinct_locale_files, instance_writer: false, default: false
300
-
301
- # Path for component files
322
+ # config.view_component.generate.sidecar = true
302
323
  #
303
- # config.view_component.view_component_path = "app/my_components"
324
+ # #### #stimulus_controller
304
325
  #
305
- # Defaults to `app/components`.
326
+ # Always generate a Stimulus controller alongside the component:
306
327
  #
307
- mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
308
-
309
- # Parent class for generated components
328
+ # config.view_component.generate.stimulus_controller = true
310
329
  #
311
- # config.view_component.component_parent_class = "MyBaseComponent"
330
+ # #### #locale
312
331
  #
313
- # Defaults to "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
332
+ # Always generate translations file alongside the component:
314
333
  #
315
- mattr_accessor :component_parent_class, instance_writer: false
316
-
317
- # Always generate a component with a sidecar directory:
334
+ # config.view_component.generate.locale = true
335
+ #
336
+ # #### #distinct_locale_files
337
+ #
338
+ # Always generate as many translations files as available locales:
339
+ #
340
+ # config.view_component.generate.distinct_locale_files = true
341
+ #
342
+ # One file will be generated for each configured `I18n.available_locales`,
343
+ # falling back to `[:en]` when no `available_locales` is defined.
344
+ #
345
+ # #### #preview
318
346
  #
319
- # config.view_component.generate_sidecar = true
347
+ # Always generate preview alongside the component:
320
348
  #
321
- # Defaults to `false`.
349
+ # config.view_component.generate.preview = true
322
350
  #
323
- mattr_accessor :generate_sidecar, instance_writer: false, default: false
351
+ # Defaults to `false`.
352
+ mattr_accessor :generate, instance_writer: false, default: ActiveSupport::OrderedOptions.new(false)
324
353
 
325
354
  class << self
326
355
  # @private
@@ -424,8 +453,8 @@ module ViewComponent
424
453
  # Do as much work as possible in this step, as doing so reduces the amount
425
454
  # of work done each time a component is rendered.
426
455
  # @private
427
- def compile(raise_errors: false)
428
- compiler.compile(raise_errors: raise_errors)
456
+ def compile(raise_errors: false, force: false)
457
+ compiler.compile(raise_errors: raise_errors, force: force)
429
458
  end
430
459
 
431
460
  # @private
@@ -20,10 +20,11 @@ module ViewComponent
20
20
 
21
21
  def invalidate_class!(klass)
22
22
  cache.delete(klass)
23
+ klass.compiler.reset_render_template_for
23
24
  end
24
25
 
25
26
  def invalidate!
26
- cache.clear
27
+ cache.each { |klass| invalidate_class!(klass) }
27
28
  end
28
29
  end
29
30
  end
@@ -27,12 +27,11 @@ module ViewComponent
27
27
  self.class.mode == DEVELOPMENT_MODE
28
28
  end
29
29
 
30
- def compile(raise_errors: false)
31
- return if compiled?
30
+ def compile(raise_errors: false, force: false)
31
+ return if compiled? && !force
32
+ return if component_class == ViewComponent::Base
32
33
 
33
34
  with_lock do
34
- CompileCache.invalidate_class!(component_class)
35
-
36
35
  subclass_instance_methods = component_class.instance_methods(false)
37
36
 
38
37
  if subclass_instance_methods.include?(:with_content) && raise_errors
@@ -49,7 +48,7 @@ module ViewComponent
49
48
  end
50
49
 
51
50
  if subclass_instance_methods.include?(:before_render_check)
52
- ActiveSupport::Deprecation.warn(
51
+ ViewComponent::Deprecation.warn(
53
52
  "`#before_render_check` will be removed in v3.0.0.\n\n" \
54
53
  "To fix this issue, use `#before_render` instead."
55
54
  )
@@ -65,8 +64,8 @@ module ViewComponent
65
64
  # as Ruby warns when redefining a method.
66
65
  method_name = call_method_name(template[:variant])
67
66
 
68
- if component_class.instance_methods.include?(method_name.to_sym)
69
- component_class.send(:undef_method, method_name.to_sym)
67
+ if component_class.instance_methods(false).include?(method_name.to_sym)
68
+ component_class.send(:remove_method, method_name.to_sym)
70
69
  end
71
70
 
72
71
  component_class.class_eval <<-RUBY, template[:path], -1
@@ -93,14 +92,18 @@ module ViewComponent
93
92
  end
94
93
  end
95
94
 
95
+ def reset_render_template_for
96
+ if component_class.instance_methods(false).include?(:render_template_for)
97
+ component_class.send(:remove_method, :render_template_for)
98
+ end
99
+ end
100
+
96
101
  private
97
102
 
98
103
  attr_reader :component_class
99
104
 
100
105
  def define_render_template_for
101
- if component_class.instance_methods.include?(:render_template_for)
102
- component_class.send(:undef_method, :render_template_for)
103
- end
106
+ reset_render_template_for
104
107
 
105
108
  variant_elsifs = variants.compact.uniq.map do |variant|
106
109
  "elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
@@ -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
  )
@@ -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"
@@ -70,6 +71,10 @@ 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
 
74
+ if HTML_SAFE_TRANSLATION_KEY.match?(key)
75
+ html_escape_translation_options!(options)
76
+ end
77
+
73
78
  if key.start_with?(i18n_scope + ".")
74
79
  translated =
75
80
  catch(:exception) do
@@ -82,7 +87,7 @@ module ViewComponent
82
87
  end
83
88
 
84
89
  if HTML_SAFE_TRANSLATION_KEY.match?(key)
85
- translated = translated.html_safe # rubocop:disable Rails/OutputSafety
90
+ translated = html_safe_translation(translated)
86
91
  end
87
92
 
88
93
  translated
@@ -96,5 +101,30 @@ module ViewComponent
96
101
  def i18n_scope
97
102
  self.class.i18n_scope
98
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
99
129
  end
100
130
  end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 49
6
+ MINOR = 51
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.49.0
4
+ version: 2.51.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-02-11 00:00:00.000000000 Z
11
+ date: 2022-03-19 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.3.3
379
+ rubygems_version: 3.2.32
377
380
  signing_key:
378
381
  specification_version: 4
379
382
  summary: View components for Rails