view_component 2.49.1 → 2.52.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: ef9372866a072103dd38b8da27bfcf57ad66012524b6a80f6caf311a97533435
4
- data.tar.gz: 654c9561c09c8cae7ca8720d126060e8196d38b0cd832d6a5a6e1f7e8c8ad133
3
+ metadata.gz: 1eb8c14aae22e2977db7cde77cd64f9ff1d69f14c0f2c95b6b2ebf0fabf67f5a
4
+ data.tar.gz: fe2c729ae013bf1dd2effb17143acfef756e915ea773dcb7eef84fd329c7e002
5
5
  SHA512:
6
- metadata.gz: fbd9bf43a06132fa0d26fba5155382be904bba9aaf181c9bb0f715a3caf103f5ae7bff74640e62c31e3ea8600a61f5405e364553b5f56f3cbc5af7a22a77f5f8
7
- data.tar.gz: 946639d557fd6dd76889c6a645187fc1235bd7e76817944429f306be1d7798ac9fe9b8005301f3ea106c564d6502d3e1bc21fc1ea5b9902784d99296979c8d93
6
+ metadata.gz: 0e72fac02dd38cd39cfa7fe45bc57617a8088d2016add0c769df9075393dc8fde66ea44c96d466019354585da3754b1db2099e9207a46418b011e6ef8765b816
7
+ data.tar.gz: 6ec82473f583cd484e9ee4042683d7d605131279ef855a199f1a89292967dd971c6ed8a43f8ad7fc9d519bae02fb4f2e5cf8d14f4876ffa1b524ad41ce25ea20
@@ -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,9 +7,94 @@ title: Changelog
7
7
 
8
8
  ## main
9
9
 
10
+ ## 2.52.0
11
+
12
+ * Add ADR for separate slot getter/setter API.
13
+
14
+ *Blake Williams*
15
+
16
+ * Add the option to use a "global" output buffer so `form_for` and friends can be used with view components.
17
+
18
+ *Cameron Dutro*, *Blake Williams*
19
+
20
+ * Fix fragment caching in partials when global output buffer is enabled.
21
+ * Fix template inheritance when eager loading is disabled.
22
+
23
+ *Cameron Dutro*
24
+
25
+ ## 2.51.0
26
+
27
+ * Update the docs only when releasing a new version.
28
+
29
+ *Hans Lemuet*
30
+
31
+ * Alphabetize companies using ViewComponent and add Brightline to the list.
32
+
33
+ *Jack Schuss*
34
+
35
+ * Add CMYK value for ViewComponent Red color on logo page.
36
+
37
+ *Dylan Smith*
38
+
39
+ * Improve performance by moving template compilation from `#render_in` to `#render_template_for`.
40
+
41
+ *Cameron Dutro*
42
+
43
+ ## 2.50.0
44
+
45
+ * Add tests for `layout` usage when rendering via controller.
46
+
47
+ *Felipe Sateler*
48
+
49
+ * Support returning Arrays from i18n files, and support marking them as HTML-safe translations.
50
+
51
+ *foca*
52
+
53
+ * Add Cometeer and Framework to users list.
54
+
55
+ *Elia Schito*
56
+
57
+ * Update Microsoft Vale styles.
58
+
59
+ *Simon Fish*
60
+
61
+ * Fix example in testing guide for how to setup default Rails tests.
62
+
63
+ *Steven Hansen*
64
+
65
+ * Update benchmark script to render multiple components/partials instead of a single instance per-run.
66
+
67
+ *Blake Williams*
68
+
69
+ * Add predicate methods `#{slot_name}?` to slots.
70
+
71
+ *Hans Lemuet*
72
+
73
+ * Use a dedicated deprecation instance, silence it while testing.
74
+
75
+ *Max Beizer, Hans Lemuet, Elia Schito*
76
+
77
+ * Fix Ruby warnings.
78
+
79
+ *Hans Lemuet*
80
+
81
+ * Place all generator options under `config.generate` namespace.
82
+
83
+ *Simon Fish*
84
+
85
+ * Allow preview generator to use provided component attributes.
86
+ * Add config option `config.view_component.generate.preview` to enable project-wide preview generation.
87
+ * Ensure all generated `.rb` files include `# frozen_string_literal: true` statement.
88
+
89
+ *Bob Maerten*
90
+
91
+ * Add Shogun to users list.
92
+
93
+ *Bernie Chiu*
94
+
10
95
  ## 2.49.1
11
96
 
12
- * Patch XSS vulnerability in `Translatable` module caused by improperly escaped interpolation arguments.
97
+ * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
13
98
 
14
99
  *Cameron Dutro*
15
100
 
@@ -619,6 +704,12 @@ title: Changelog
619
704
 
620
705
  *Joel Hawksley*
621
706
 
707
+ ## 2.31.2
708
+
709
+ * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
710
+
711
+ *Cameron Dutro*
712
+
622
713
  ## 2.31.1
623
714
 
624
715
  * Fix `DEPRECATION WARNING: before_render_check` when compiling `ViewComponent::Base`
@@ -663,12 +754,6 @@ _Note: This release includes an underlying change to Slots that may affect incor
663
754
 
664
755
  *Joel Hawksley*
665
756
 
666
- ## 2.29.1
667
-
668
- * Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments.
669
-
670
- *Cameron Dutro*
671
-
672
757
  ## 2.29.0
673
758
 
674
759
  * Allow Slot lambdas to share data from the parent component and allow chaining on the returned component.
@@ -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,11 +66,11 @@ 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
 
72
+ @output_buffer = ActionView::OutputBuffer.new unless @global_buffer_in_use
73
+
57
74
  @lookup_context ||= view_context.lookup_context
58
75
 
59
76
  # required for path helpers in older Rails versions
@@ -87,7 +104,7 @@ module ViewComponent
87
104
  before_render
88
105
 
89
106
  if render?
90
- render_template_for(@__vc_variant).to_s + _output_postamble
107
+ perform_render
91
108
  else
92
109
  ""
93
110
  end
@@ -95,6 +112,20 @@ module ViewComponent
95
112
  @current_template = old_current_template
96
113
  end
97
114
 
115
+ def perform_render
116
+ render_template_for(@__vc_variant).to_s + _output_postamble
117
+ end
118
+
119
+ # :nocov:
120
+ def render_template_for(variant = nil)
121
+ # Force compilation here so the compiler always redefines render_template_for.
122
+ # This is mostly a safeguard to prevent infinite recursion.
123
+ self.class.compile(raise_errors: true, force: true)
124
+ # .compile replaces this method; call the new one
125
+ render_template_for(variant)
126
+ end
127
+ # :nocov:
128
+
98
129
  # EXPERIMENTAL: Optional content to be returned after the rendered template.
99
130
  #
100
131
  # @return [String]
@@ -218,14 +249,11 @@ module ViewComponent
218
249
  # @param variant [Symbol] The variant to be used by the component.
219
250
  # @return [self]
220
251
  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
252
  @__vc_variant = variant
226
253
 
227
254
  self
228
255
  end
256
+ deprecate :with_variant, deprecator: ViewComponent::Deprecation
229
257
 
230
258
  # The current request. Use sparingly as doing so introduces coupling that
231
259
  # inhibits encapsulation & reuse, often making testing difficult.
@@ -271,56 +299,63 @@ module ViewComponent
271
299
  #
272
300
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
273
301
 
274
- # Always generate a Stimulus controller alongside the component:
302
+ # Path for component files
275
303
  #
276
- # config.view_component.generate_stimulus_controller = true
304
+ # config.view_component.view_component_path = "app/my_components"
277
305
  #
278
- # Defaults to `false`.
306
+ # Defaults to `app/components`.
279
307
  #
280
- mattr_accessor :generate_stimulus_controller, instance_writer: false, default: false
308
+ mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
281
309
 
282
- # Always generate translations file alongside the component:
310
+ # Parent class for generated components
283
311
  #
284
- # config.view_component.generate_locale = true
312
+ # config.view_component.component_parent_class = "MyBaseComponent"
285
313
  #
286
- # Defaults to `false`.
314
+ # Defaults to nil. If this is falsy, generators will use
315
+ # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
287
316
  #
288
- mattr_accessor :generate_locale, instance_writer: false, default: false
317
+ mattr_accessor :component_parent_class, instance_writer: false
289
318
 
290
- # Always generate as many translations files as available locales:
319
+ # Configuration for generators.
291
320
  #
292
- # config.view_component.generate_distinct_locale_files = true
321
+ # All options under this namespace default to `false` unless otherwise
322
+ # stated.
293
323
  #
294
- # Defaults to `false`.
324
+ # #### #sidecar
295
325
  #
296
- # One file will be generated for each configured `I18n.available_locales`.
297
- # Fallback on `[:en]` when no available_locales is defined.
326
+ # Always generate a component with a sidecar directory:
298
327
  #
299
- mattr_accessor :generate_distinct_locale_files, instance_writer: false, default: false
300
-
301
- # Path for component files
328
+ # config.view_component.generate.sidecar = true
302
329
  #
303
- # config.view_component.view_component_path = "app/my_components"
330
+ # #### #stimulus_controller
304
331
  #
305
- # Defaults to `app/components`.
332
+ # Always generate a Stimulus controller alongside the component:
306
333
  #
307
- mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
308
-
309
- # Parent class for generated components
334
+ # config.view_component.generate.stimulus_controller = true
310
335
  #
311
- # config.view_component.component_parent_class = "MyBaseComponent"
336
+ # #### #locale
337
+ #
338
+ # Always generate translations file alongside the component:
312
339
  #
313
- # Defaults to "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
340
+ # config.view_component.generate.locale = true
314
341
  #
315
- mattr_accessor :component_parent_class, instance_writer: false
316
-
317
- # Always generate a component with a sidecar directory:
342
+ # #### #distinct_locale_files
343
+ #
344
+ # Always generate as many translations files as available locales:
318
345
  #
319
- # config.view_component.generate_sidecar = true
346
+ # config.view_component.generate.distinct_locale_files = true
320
347
  #
321
- # Defaults to `false`.
348
+ # One file will be generated for each configured `I18n.available_locales`,
349
+ # falling back to `[:en]` when no `available_locales` is defined.
322
350
  #
323
- mattr_accessor :generate_sidecar, instance_writer: false, default: false
351
+ # #### #preview
352
+ #
353
+ # Always generate preview alongside the component:
354
+ #
355
+ # config.view_component.generate.preview = true
356
+ #
357
+ # Defaults to `false`.
358
+ mattr_accessor :generate, instance_writer: false, default: ActiveSupport::OrderedOptions.new(false)
324
359
 
325
360
  class << self
326
361
  # @private
@@ -392,6 +427,22 @@ module ViewComponent
392
427
  # `compile` defines
393
428
  compile
394
429
 
430
+ # Give the child its own personal #render_template_for to protect against the case when
431
+ # eager loading is disabled and the parent component is rendered before the child. In
432
+ # such a scenario, the parent will override ViewComponent::Base#render_template_for,
433
+ # meaning it will not be called for any children and thus not compile their templates.
434
+ if !child.instance_methods(false).include?(:render_template_for) && !child.compiled?
435
+ child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
436
+ def render_template_for(variant = nil)
437
+ # Force compilation here so the compiler always redefines render_template_for.
438
+ # This is mostly a safeguard to prevent infinite recursion.
439
+ self.class.compile(raise_errors: true, force: true)
440
+ # .compile replaces this method; call the new one
441
+ render_template_for(variant)
442
+ end
443
+ RUBY
444
+ end
445
+
395
446
  # If Rails application is loaded, add application url_helpers to the component context
396
447
  # we need to check this to use this gem as a dependency
397
448
  if defined?(Rails) && Rails.application
@@ -424,8 +475,8 @@ module ViewComponent
424
475
  # Do as much work as possible in this step, as doing so reduces the amount
425
476
  # of work done each time a component is rendered.
426
477
  # @private
427
- def compile(raise_errors: false)
428
- compiler.compile(raise_errors: raise_errors)
478
+ def compile(raise_errors: false, force: false)
479
+ compiler.compile(raise_errors: raise_errors, force: force)
429
480
  end
430
481
 
431
482
  # @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,13 +64,12 @@ 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
- component_class.class_eval <<-RUBY, template[:path], -1
71
+ component_class.class_eval <<-RUBY, template[:path], 0
73
72
  def #{method_name}
74
- @output_buffer = ActionView::OutputBuffer.new
75
73
  #{compiled_template(template[:path])}
76
74
  end
77
75
  RUBY
@@ -93,14 +91,18 @@ module ViewComponent
93
91
  end
94
92
  end
95
93
 
94
+ def reset_render_template_for
95
+ if component_class.instance_methods(false).include?(:render_template_for)
96
+ component_class.send(:remove_method, :render_template_for)
97
+ end
98
+ end
99
+
96
100
  private
97
101
 
98
102
  attr_reader :component_class
99
103
 
100
104
  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
105
+ reset_render_template_for
104
106
 
105
107
  variant_elsifs = variants.compact.uniq.map do |variant|
106
108
  "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
@@ -20,6 +20,7 @@ module ViewComponent
20
20
  options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
21
21
  options.preview_route ||= ViewComponent::Base.preview_route
22
22
  options.preview_controller ||= ViewComponent::Base.preview_controller
23
+ options.use_global_output_buffer = false if options.use_global_output_buffer.nil?
23
24
 
24
25
  if options.show_previews
25
26
  options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?(
@@ -27,7 +28,7 @@ module ViewComponent
27
28
  )
28
29
 
29
30
  if options.preview_path.present?
30
- ActiveSupport::Deprecation.warn(
31
+ ViewComponent::Deprecation.warn(
31
32
  "`preview_path` will be removed in v3.0.0. Use `preview_paths` instead."
32
33
  )
33
34
  options.preview_paths << options.preview_path
@@ -57,6 +58,21 @@ module ViewComponent
57
58
  end
58
59
  end
59
60
 
61
+ initializer "view_component.enable_global_output_buffer" do |app|
62
+ ActiveSupport.on_load(:view_component) do
63
+ env_use_gob = ENV.fetch("VIEW_COMPONENT_USE_GLOBAL_OUTPUT_BUFFER", "false") == "true"
64
+ config_use_gob = app.config.view_component.use_global_output_buffer
65
+
66
+ if config_use_gob || env_use_gob
67
+ # :nocov:
68
+ app.config.view_component.use_global_output_buffer = true
69
+ ViewComponent::Base.prepend(ViewComponent::GlobalOutputBuffer)
70
+ ActionView::Base.prepend(ViewComponent::GlobalOutputBuffer::ActionViewMods)
71
+ # :nocov:
72
+ end
73
+ end
74
+ end
75
+
60
76
  initializer "view_component.set_autoload_paths" do |app|
61
77
  options = app.config.view_component
62
78
 
@@ -155,7 +171,9 @@ end
155
171
 
156
172
  # :nocov:
157
173
  unless defined?(ViewComponent::Base)
158
- ActiveSupport::Deprecation.warn(
174
+ require "view_component/deprecation"
175
+
176
+ ViewComponent::Deprecation.warn(
159
177
  "This manually engine loading is deprecated and will be removed in v3.0.0. " \
160
178
  "Remove `require \"view_component/engine\"`."
161
179
  )
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module GlobalOutputBuffer
5
+ def render_in(view_context, &block)
6
+ unless view_context.output_buffer.is_a?(OutputBufferStack)
7
+ # use instance_variable_set here to avoid triggering the code in the #output_buffer= method below
8
+ view_context.instance_variable_set(:@output_buffer, OutputBufferStack.new(view_context.output_buffer))
9
+ end
10
+
11
+ @output_buffer = view_context.output_buffer
12
+ @global_buffer_in_use = true
13
+
14
+ super(view_context, &block)
15
+ end
16
+
17
+ def perform_render
18
+ # HAML unhelpfully assigns to @output_buffer directly, so we hold onto a reference to
19
+ # it and restore @output_buffer when the HAML engine is finished. In non-HAML cases,
20
+ # @output_buffer and orig_buf will point to the same object, making the reassignment
21
+ # statements no-ops.
22
+ orig_buf = @output_buffer
23
+ @output_buffer.push
24
+ result = render_template_for(@__vc_variant).to_s + _output_postamble
25
+ @output_buffer = orig_buf
26
+ @output_buffer.pop
27
+ result
28
+ end
29
+
30
+ def output_buffer=(other_buffer)
31
+ @output_buffer.replace(other_buffer)
32
+ end
33
+
34
+ def with_output_buffer(buf = nil)
35
+ unless buf
36
+ buf = ActionView::OutputBuffer.new
37
+ if output_buffer && output_buffer.respond_to?(:encoding)
38
+ buf.force_encoding(output_buffer.encoding)
39
+ end
40
+ end
41
+
42
+ output_buffer.push(buf)
43
+ result = nil
44
+
45
+ begin
46
+ yield
47
+ ensure
48
+ # assign result here to avoid a return statement, which will
49
+ # immediately return to the caller and swallow any errors
50
+ result = output_buffer.pop
51
+ end
52
+
53
+ result
54
+ end
55
+
56
+ module ActionViewMods
57
+ def output_buffer=(other_buffer)
58
+ if @output_buffer.is_a?(OutputBufferStack)
59
+ @output_buffer.replace(other_buffer)
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ def with_output_buffer(buf = nil)
66
+ unless buf
67
+ buf = ActionView::OutputBuffer.new
68
+ if @output_buffer && @output_buffer.respond_to?(:encoding)
69
+ buf.force_encoding(@output_buffer.encoding)
70
+ end
71
+ end
72
+
73
+ result = nil
74
+
75
+ if @output_buffer.is_a?(OutputBufferStack)
76
+ @output_buffer.push(buf)
77
+
78
+ begin
79
+ yield
80
+ ensure
81
+ result = @output_buffer.pop
82
+ end
83
+
84
+ result
85
+ else
86
+ @output_buffer, old_buffer = buf, output_buffer
87
+
88
+ begin
89
+ yield
90
+ ensure
91
+ @output_buffer = old_buffer
92
+ end
93
+
94
+ buf
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ class OutputBufferStack
5
+ delegate_missing_to :@current_buffer
6
+ delegate :presence, :present?, :html_safe?, to: :@current_buffer
7
+
8
+ attr_reader :buffer_stack
9
+
10
+ def self.make_frame(*args)
11
+ ActionView::OutputBuffer.new(*args)
12
+ end
13
+
14
+ def initialize(initial_buffer = nil)
15
+ if initial_buffer.is_a?(self.class)
16
+ @current_buffer = self.class.make_frame(initial_buffer.current)
17
+ @buffer_stack = [*initial_buffer.buffer_stack[0..-2], @current_buffer]
18
+ else
19
+ @current_buffer = initial_buffer || self.class.make_frame
20
+ @buffer_stack = [@current_buffer]
21
+ end
22
+ end
23
+
24
+ def replace(buffer)
25
+ return if self == buffer
26
+
27
+ @current_buffer = buffer.current
28
+ @buffer_stack = buffer.buffer_stack
29
+ end
30
+
31
+ def append=(arg)
32
+ @current_buffer.append = arg
33
+ end
34
+
35
+ def safe_append=(arg)
36
+ @current_buffer.safe_append = arg
37
+ end
38
+
39
+ def safe_concat(arg)
40
+ # rubocop:disable Rails/OutputSafety
41
+ @current_buffer.safe_concat(arg)
42
+ # rubocop:enable Rails/OutputSafety
43
+ end
44
+
45
+ def length
46
+ @current_buffer.length
47
+ end
48
+
49
+ def push(buffer = nil)
50
+ buffer ||= self.class.make_frame
51
+ @buffer_stack.push(buffer)
52
+ @current_buffer = buffer
53
+ end
54
+
55
+ def pop
56
+ @buffer_stack.pop.tap do
57
+ @current_buffer = @buffer_stack.last
58
+ end
59
+ end
60
+
61
+ def to_s
62
+ @current_buffer
63
+ end
64
+
65
+ alias_method :current, :to_s
66
+ end
67
+ 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?
@@ -45,18 +45,12 @@ module ViewComponent
45
45
  if defined?(@__vc_content_set_by_with_content)
46
46
  @__vc_component_instance.with_content(@__vc_content_set_by_with_content)
47
47
 
48
- view_context.capture do
49
- @__vc_component_instance.render_in(view_context)
50
- end
48
+ @__vc_component_instance.render_in(view_context)
51
49
  elsif defined?(@__vc_content_block)
52
- view_context.capture do
53
- # render_in is faster than `parent.render`
54
- @__vc_component_instance.render_in(view_context, &@__vc_content_block)
55
- end
50
+ # render_in is faster than `parent.render`
51
+ @__vc_component_instance.render_in(view_context, &@__vc_content_block)
56
52
  else
57
- view_context.capture do
58
- @__vc_component_instance.render_in(view_context)
59
- end
53
+ @__vc_component_instance.render_in(view_context)
60
54
  end
61
55
  elsif defined?(@__vc_content)
62
56
  @__vc_content
@@ -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)
@@ -87,7 +87,7 @@ module ViewComponent
87
87
  end
88
88
 
89
89
  if HTML_SAFE_TRANSLATION_KEY.match?(key)
90
- translated = translated.html_safe # rubocop:disable Rails/OutputSafety
90
+ translated = html_safe_translation(translated)
91
91
  end
92
92
 
93
93
  translated
@@ -3,8 +3,8 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 49
7
- PATCH = 1
6
+ MINOR = 52
7
+ PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
10
10
  end
@@ -10,7 +10,10 @@ module ViewComponent
10
10
  autoload :Compiler
11
11
  autoload :CompileCache
12
12
  autoload :ComponentError
13
+ autoload :Deprecation
14
+ autoload :GlobalOutputBuffer
13
15
  autoload :Instrumentation
16
+ autoload :OutputBufferStack
14
17
  autoload :Preview
15
18
  autoload :PreviewTemplateError
16
19
  autoload :TestHelpers
@@ -21,8 +24,8 @@ end
21
24
 
22
25
  # :nocov:
23
26
  if defined?(ViewComponent::Engine)
24
- ActiveSupport::Deprecation.warn(
25
- "This manually engine loading is deprecated and will be removed in v3.0.0. " \
27
+ ViewComponent::Deprecation.warn(
28
+ "Manually loading the engine is deprecated and will be removed in v3.0.0. " \
26
29
  "Remove `require \"view_component/engine\"`."
27
30
  )
28
31
  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.1
4
+ version: 2.52.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-03-02 00:00:00.000000000 Z
11
+ date: 2022-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -329,8 +329,13 @@ 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
336
+ - lib/view_component/global_output_buffer.rb
333
337
  - lib/view_component/instrumentation.rb
338
+ - lib/view_component/output_buffer_stack.rb
334
339
  - lib/view_component/polymorphic_slots.rb
335
340
  - lib/view_component/preview.rb
336
341
  - lib/view_component/preview_template_error.rb