view_component 2.49.0 → 2.51.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: 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