view_component 2.49.1 → 2.52.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: 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