view_component 4.0.0.alpha2 → 4.0.0.alpha4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35393079b843dba053e2e4acca6429b8181a5c255c5843c7ad39fcb5245b8762
4
- data.tar.gz: a9a602a05dd03d1865bfef3115904f8f7afec7bccbddc2497469638982259875
3
+ metadata.gz: 70feada9d7f9456623bfed2a6c76a9546f8fe5596f81e0f47ed7059808b1a68d
4
+ data.tar.gz: 77d8002e2a0fdb344c3647972b9051ce1c3ac3bc299e49442c8631921a6b5437
5
5
  SHA512:
6
- metadata.gz: 04e1d26d5f24f76c75cf4b105842b71ce31887ec8ec939e6c9a232f1c0bca48fe61bdad32e602773aa95f19c338a2d2fc4962a381fc025bf3ea356d1343cbf80
7
- data.tar.gz: b2c254c725e460a0dc16cd90300218ffff424c889946b7df9e31561b93d89dba304510e07afd48d014fdf1f576d07aa9e59ac75e2dce0822fb45fac7b7f9fdf1
6
+ metadata.gz: c3aeae640580d7d5c9192836fbc8ab26932a9527117438f4fa808fed36cd2c4393194fd66e5d01ddbc56bd0f0d5042b1707ee25b94760969b06a9d34a1700f85
7
+ data.tar.gz: cb4ee57d927b7b9a27436ec1d736675ee59fe2dfc7aa281e8c6a1b9be87fa517849bc3443a6057a01f1a3e1b97320e8d566681dc989c452fa8c45ff261e058c9
data/docs/CHANGELOG.md CHANGED
@@ -10,6 +10,26 @@ nav_order: 6
10
10
 
11
11
  ## main
12
12
 
13
+ ## 4.0.0.alpha4
14
+
15
+ * BREAKING: Remove default initializer from `ViewComponent::Base`. Previously, `ViewComponent::Base` defined a catch-all initializer that allowed components without an initializer defined to be passed arbitrary arguments.
16
+
17
+ *Joel Hawksley*
18
+
19
+ * Graduate `SlotableDefault` to be included by default.
20
+
21
+ *Joel Hawksley*
22
+
23
+ * Fix bug in `SlotableDefault` where default couldn't be overridden when content was passed as a block.
24
+
25
+ *Bill Watts*, *Joel Hawksley*
26
+
27
+ ## 4.0.0.alpha3
28
+
29
+ * BREAKING: Remove dependency on `ActionView::Base`, eliminating the need for capture compatibility patch.
30
+
31
+ *Cameron Dutro*
32
+
13
33
  ## 4.0.0.alpha2
14
34
 
15
35
  * Add `#current_template` accessor and `Template#path` for diagnostic usage.
@@ -118,6 +138,14 @@ This release makes the following breaking changes:
118
138
 
119
139
  *Tiago Menegaz*, *Joel Hawksley*
120
140
 
141
+ * Introduce component-local config and migrate `strip_trailing_whitespace` to use it under the hood.
142
+
143
+ *Simon Fish*
144
+
145
+ * Add docs about Slack channel in Ruby Central workspace. (Join us! #oss-view-component). Email joelhawksley@github.com for an invite.
146
+
147
+ *Joel Hawksley
148
+
121
149
  * Do not include internal `DocsBuilderComponent` or `YARD::MattrAccessorHandler` in published gem.
122
150
 
123
151
  *Joel Hawksley*
@@ -5,20 +5,33 @@ require "active_support/configurable"
5
5
  require "view_component/collection"
6
6
  require "view_component/compile_cache"
7
7
  require "view_component/compiler"
8
+ require "view_component/component_local_config"
8
9
  require "view_component/config"
9
10
  require "view_component/errors"
10
11
  require "view_component/inline_template"
11
12
  require "view_component/preview"
12
13
  require "view_component/request_details"
13
14
  require "view_component/slotable"
14
- require "view_component/slotable_default"
15
15
  require "view_component/template"
16
16
  require "view_component/translatable"
17
17
  require "view_component/with_content_helper"
18
18
  require "view_component/use_helpers"
19
19
 
20
+ module ActionView
21
+ class OutputBuffer
22
+ def with_buffer(buf = nil)
23
+ new_buffer = buf || +""
24
+ old_buffer, @raw_buffer = @raw_buffer, new_buffer
25
+ yield
26
+ new_buffer
27
+ ensure
28
+ @raw_buffer = old_buffer
29
+ end
30
+ end
31
+ end
32
+
20
33
  module ViewComponent
21
- class Base < ActionView::Base
34
+ class Base
22
35
  class << self
23
36
  delegate(*ViewComponent::Config.defaults.keys, to: :config)
24
37
 
@@ -34,21 +47,26 @@ module ViewComponent
34
47
  end
35
48
  end
36
49
 
50
+ include ActionView::Helpers
51
+ include ERB::Escape
52
+ include ActiveSupport::CoreExt::ERBUtil
53
+
37
54
  include ViewComponent::InlineTemplate
38
55
  include ViewComponent::UseHelpers
39
56
  include ViewComponent::Slotable
40
57
  include ViewComponent::Translatable
41
58
  include ViewComponent::WithContentHelper
59
+ include ViewComponent::ComponentLocalConfig
42
60
 
43
61
  # For CSRF authenticity tokens in forms
44
62
  delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
45
63
 
64
+ # HTML construction methods
65
+ delegate :output_buffer, :lookup_context, :view_renderer, :view_flow, to: :helpers
66
+
46
67
  # For Content Security Policy nonces
47
68
  delegate :content_security_policy_nonce, to: :helpers
48
69
 
49
- # Config option that strips trailing whitespace in templates before compiling them.
50
- class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false, default: false
51
-
52
70
  attr_accessor :__vc_original_view_context
53
71
  attr_reader :current_template
54
72
 
@@ -62,7 +80,7 @@ module ViewComponent
62
80
  # @param view_context [ActionView::Base] The original view context.
63
81
  # @return [void]
64
82
  def set_original_view_context(view_context)
65
- self.__vc_original_view_context = view_context
83
+ # noop
66
84
  end
67
85
 
68
86
  using RequestDetails
@@ -81,7 +99,7 @@ module ViewComponent
81
99
  @view_context = view_context
82
100
  self.__vc_original_view_context ||= view_context
83
101
 
84
- @output_buffer = ActionView::OutputBuffer.new
102
+ @output_buffer = view_context.output_buffer
85
103
 
86
104
  @lookup_context ||= view_context.lookup_context
87
105
 
@@ -108,14 +126,20 @@ module ViewComponent
108
126
  before_render
109
127
 
110
128
  if render?
111
- rendered_template = render_template_for(@__vc_requested_details).to_s
129
+ value = nil
130
+
131
+ @output_buffer.with_buffer do
132
+ rendered_template = render_template_for(@__vc_requested_details).to_s
112
133
 
113
- # Avoid allocating new string when output_preamble and output_postamble are blank
114
- if output_preamble.blank? && output_postamble.blank?
115
- rendered_template
116
- else
117
- safe_output_preamble + rendered_template + safe_output_postamble
134
+ # Avoid allocating new string when output_preamble and output_postamble are blank
135
+ value = if output_preamble.blank? && output_postamble.blank?
136
+ rendered_template
137
+ else
138
+ safe_output_preamble + rendered_template + safe_output_postamble
139
+ end
118
140
  end
141
+
142
+ value
119
143
  else
120
144
  ""
121
145
  end
@@ -191,12 +215,6 @@ module ViewComponent
191
215
  true
192
216
  end
193
217
 
194
- # Override the ActionView::Base initializer so that components
195
- # do not need to define their own initializers.
196
- # @private
197
- def initialize(*)
198
- end
199
-
200
218
  # Re-use original view_context if we're not rendering a component.
201
219
  #
202
220
  # This prevents an exception when rendering a partial inside of a component that has also been rendered outside
@@ -207,7 +225,7 @@ module ViewComponent
207
225
  def render(options = {}, args = {}, &block)
208
226
  if options.respond_to?(:set_original_view_context)
209
227
  options.set_original_view_context(self.__vc_original_view_context)
210
- super
228
+ @view_context.render(options, args, &block)
211
229
  else
212
230
  __vc_original_view_context.render(options, args, &block)
213
231
  end
@@ -249,7 +267,7 @@ module ViewComponent
249
267
  raise e, <<~MESSAGE.chomp if view_context && e.is_a?(NameError) && helpers.respond_to?(method_name)
250
268
  #{e.message}
251
269
 
252
- You may be trying to call a method provided as a view helper. Did you mean `helpers.#{method_name}'?
270
+ You may be trying to call a method provided as a view helper. Did you mean `helpers.#{method_name}`?
253
271
  MESSAGE
254
272
 
255
273
  raise
@@ -594,16 +612,38 @@ module ViewComponent
594
612
  # end
595
613
  # ```
596
614
  #
615
+ # @deprecated Use the new component-local configuration option instead.
616
+ #
617
+ # ```ruby
618
+ # class MyComponent < ViewComponent::Base
619
+ # configure_view_component do |config|
620
+ # config.strip_trailing_whitespace = true
621
+ # end
622
+ # end
623
+ # ```
624
+ #
597
625
  # @param value [Boolean] Whether to strip newlines.
598
626
  def strip_trailing_whitespace(value = true)
599
- self.__vc_strip_trailing_whitespace = value
627
+ ViewComponent::Deprecation.deprecation_warning(
628
+ "strip_trailing_whitespace",
629
+ <<~DOC
630
+ Use the new component-local configuration option instead:
631
+
632
+ class #{self.class.name} < ViewComponent::Base
633
+ configure_view_component do |config|
634
+ config.strip_trailing_whitespace = #{value}
635
+ end
636
+ end
637
+ DOC
638
+ )
639
+ view_component_config.strip_trailing_whitespace = value
600
640
  end
601
641
 
602
642
  # Whether trailing whitespace will be stripped before compilation.
603
643
  #
604
644
  # @return [Boolean]
605
645
  def strip_trailing_whitespace?
606
- __vc_strip_trailing_whitespace
646
+ view_component_config.strip_trailing_whitespace
607
647
  end
608
648
 
609
649
  # Ensure the component initializer accepts the
@@ -9,15 +9,8 @@ module ViewComponent
9
9
 
10
10
  delegate :size, to: :@collection
11
11
 
12
- attr_accessor :__vc_original_view_context
13
-
14
- def set_original_view_context(view_context)
15
- self.__vc_original_view_context = view_context
16
- end
17
-
18
12
  def render_in(view_context, &block)
19
13
  components.map do |component|
20
- component.set_original_view_context(__vc_original_view_context)
21
14
  component.render_in(view_context, &block)
22
15
  end.join(rendered_spacer(view_context)).html_safe
23
16
  end
@@ -67,7 +60,6 @@ module ViewComponent
67
60
 
68
61
  def rendered_spacer(view_context)
69
62
  if @spacer_component
70
- @spacer_component.set_original_view_context(__vc_original_view_context)
71
63
  @spacer_component.render_in(view_context)
72
64
  else
73
65
  ""
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module ComponentLocalConfig
5
+ class Configuration
6
+ def self.defaults
7
+ ActiveSupport::Configurable::Configuration[
8
+ strip_trailing_whitespace: false
9
+ ]
10
+ end
11
+
12
+ def initialize(config = defaults)
13
+ @config = config
14
+ end
15
+
16
+ delegate_missing_to :@config
17
+
18
+ def inheritable_copy
19
+ self.class.new(@config.inheritable_copy)
20
+ end
21
+
22
+ private
23
+
24
+ delegate :defaults, to: :class
25
+ end
26
+
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ # :nocov:
31
+ def view_component_config
32
+ @__vc_config ||= self.class.view_component_config.inheritable_copy
33
+ end
34
+
35
+ private
36
+
37
+ def inherited(child)
38
+ child.instance_variable_set(:@__vc_config, nil)
39
+ super
40
+ end
41
+ # :nocov:
42
+ end
43
+
44
+ class_methods do
45
+ def view_component_config
46
+ @__vc_config ||= if respond_to?(:superclass) && superclass.respond_to?(:view_component_config)
47
+ superclass.view_component_config.inheritable_copy
48
+ else
49
+ # create a new "anonymous" class that will host the compiled reader methods
50
+ ViewComponent::ComponentLocalConfig::Configuration.new
51
+ end
52
+ end
53
+
54
+ def configure_view_component(&block)
55
+ view_component_config.instance_eval(&block)
56
+ view_component_config.compile_methods!
57
+ end
58
+ end
59
+ end
60
+ end
@@ -21,8 +21,7 @@ module ViewComponent
21
21
  show_previews: Rails.env.development? || Rails.env.test?,
22
22
  preview_paths: default_preview_paths,
23
23
  test_controller: "ApplicationController",
24
- default_preview_layout: nil,
25
- capture_compatibility_patch_enabled: false
24
+ default_preview_layout: nil
26
25
  })
27
26
  end
28
27
 
@@ -145,13 +144,6 @@ module ViewComponent
145
144
  # previews.
146
145
  # Defaults to `nil`. If this is falsy, `"component_preview"` is used.
147
146
 
148
- # @!attribute capture_compatibility_patch_enabled
149
- # @return [Boolean]
150
- # Enables the experimental capture compatibility patch that makes ViewComponent
151
- # compatible with forms, capture, and other built-ins.
152
- # previews.
153
- # Defaults to `false`.
154
-
155
147
  def default_preview_paths
156
148
  (default_rails_preview_paths + default_rails_engines_preview_paths).uniq
157
149
  end
@@ -51,12 +51,6 @@ module ViewComponent
51
51
  end
52
52
  end
53
53
 
54
- initializer "view_component.enable_capture_patch" do |app|
55
- ActiveSupport.on_load(:view_component) do
56
- ActionView::Base.include(ViewComponent::CaptureCompatibility) if app.config.view_component.capture_compatibility_patch_enabled
57
- end
58
- end
59
-
60
54
  initializer "view_component.set_autoload_paths" do |app|
61
55
  options = app.config.view_component
62
56
 
@@ -66,6 +60,39 @@ module ViewComponent
66
60
  end
67
61
  end
68
62
 
63
+ initializer "view_component.propshaft_support" do |_app|
64
+ ActiveSupport.on_load(:view_component) do
65
+ if defined?(Propshaft)
66
+ include Propshaft::Helper
67
+ end
68
+ end
69
+ end
70
+
71
+ config.after_initialize do |app|
72
+ ActiveSupport.on_load(:view_component) do
73
+ if defined?(Sprockets::Rails)
74
+ include Sprockets::Rails::Helper
75
+
76
+ # Copy relevant config to VC context
77
+ # See: https://github.com/rails/sprockets-rails/blob/266ec49f3c7c96018dd75f9dc4f9b62fe3f7eecf/lib/sprockets/railtie.rb#L245
78
+ self.debug_assets = app.config.assets.debug
79
+ self.digest_assets = app.config.assets.digest
80
+ self.assets_prefix = app.config.assets.prefix
81
+ self.assets_precompile = app.config.assets.precompile
82
+
83
+ self.assets_environment = app.assets
84
+ self.assets_manifest = app.assets_manifest
85
+
86
+ self.resolve_assets_with = app.config.assets.resolve_with
87
+
88
+ self.check_precompiled_asset = app.config.assets.check_precompiled_asset
89
+ self.unknown_asset_fallback = app.config.assets.unknown_asset_fallback
90
+ # Expose the app precompiled asset check to the view
91
+ self.precompiled_asset_checker = ->(logical_path) { app.asset_precompiled? logical_path }
92
+ end
93
+ end
94
+ end
95
+
69
96
  initializer "view_component.eager_load_actions" do
70
97
  ActiveSupport.on_load(:after_initialize) do
71
98
  ViewComponent::Base.descendants.each(&:__vc_compile) if Rails.application.config.eager_load
@@ -58,15 +58,7 @@ module ViewComponent
58
58
  if defined?(@__vc_content_block)
59
59
  # render_in is faster than `parent.render`
60
60
  @__vc_component_instance.render_in(view_context) do |*args|
61
- return @__vc_content_block.call(*args) if @__vc_content_block&.source_location.nil?
62
-
63
- block_context = @__vc_content_block.binding.receiver
64
-
65
- if block_context.class < ActionView::Base
66
- block_context.capture(*args, &@__vc_content_block)
67
- else
68
- @__vc_content_block.call(*args)
69
- end
61
+ @__vc_content_block.call(*args)
70
62
  end
71
63
  else
72
64
  @__vc_component_instance.render_in(view_context)
@@ -351,16 +351,26 @@ module ViewComponent
351
351
  end
352
352
 
353
353
  def get_slot(slot_name)
354
+ @__vc_set_slots ||= {}
354
355
  content unless content_evaluated? # ensure content is loaded so slots will be defined
355
356
 
356
- slot = self.class.registered_slots[slot_name]
357
- @__vc_set_slots ||= {}
357
+ # If the slot is set, return it
358
+ return @__vc_set_slots[slot_name] if @__vc_set_slots[slot_name]
358
359
 
359
- if @__vc_set_slots[slot_name]
360
- return @__vc_set_slots[slot_name]
361
- end
360
+ # If there is a default method for the slot, call it
361
+ if (default_method = registered_slots[slot_name][:default_method])
362
+ renderable_value = send(default_method)
363
+ slot = Slot.new(self)
364
+
365
+ if renderable_value.respond_to?(:render_in)
366
+ slot.__vc_component_instance = renderable_value
367
+ else
368
+ slot.__vc_content = renderable_value
369
+ end
362
370
 
363
- if slot[:collection]
371
+ slot
372
+ elsif self.class.registered_slots[slot_name][:collection]
373
+ # If empty slot is a collection, return an empty array
364
374
  []
365
375
  end
366
376
  end
@@ -98,8 +98,9 @@ module ViewComponent
98
98
  @component.silence_redefinition_of_method(call_method_name)
99
99
 
100
100
  # rubocop:disable Style/EvalWithLocation
101
- @component.class_eval <<~RUBY, @path, @lineno
101
+ @component.class_eval <<~RUBY, @path, @lineno - 1
102
102
  def #{call_method_name}
103
+ @view_context.instance_variable_set(:@virtual_path, virtual_path)
103
104
  #{compiled_source}
104
105
  end
105
106
  RUBY
@@ -93,7 +93,7 @@ module ViewComponent
93
93
  def translate(key = nil, **options)
94
94
  raise ViewComponent::TranslateCalledBeforeRenderError if view_context.nil?
95
95
 
96
- return super unless __vc_i18n_backend
96
+ return @view_context.translate(key, **options) unless __vc_i18n_backend
97
97
  return key.map { |k| translate(k, **options) } if key.is_a?(Array)
98
98
 
99
99
  locale = options.delete(:locale) || ::I18n.locale
@@ -110,13 +110,13 @@ module ViewComponent
110
110
 
111
111
  # Fallback to the global translations
112
112
  if translated.is_a? ::I18n::MissingTranslation
113
- return super(key, locale: locale, **options)
113
+ return @view_context.translate(key, locale: locale, **options)
114
114
  end
115
115
 
116
116
  translated = html_safe_translation(translated) if as_html
117
117
  translated
118
118
  else
119
- super(key, locale: locale, **options)
119
+ @view_context.translate(key, locale: locale, **options)
120
120
  end
121
121
  end
122
122
  alias_method :t, :translate
@@ -5,7 +5,7 @@ module ViewComponent
5
5
  MAJOR = 4
6
6
  MINOR = 0
7
7
  PATCH = 0
8
- PRE = "alpha2"
8
+ PRE = "alpha4"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0.alpha2
4
+ version: 4.0.0.alpha4
5
5
  platform: ruby
6
6
  authors:
7
7
  - ViewComponent Team
@@ -466,10 +466,10 @@ files:
466
466
  - docs/CHANGELOG.md
467
467
  - lib/view_component.rb
468
468
  - lib/view_component/base.rb
469
- - lib/view_component/capture_compatibility.rb
470
469
  - lib/view_component/collection.rb
471
470
  - lib/view_component/compile_cache.rb
472
471
  - lib/view_component/compiler.rb
472
+ - lib/view_component/component_local_config.rb
473
473
  - lib/view_component/config.rb
474
474
  - lib/view_component/configurable.rb
475
475
  - lib/view_component/deprecation.rb
@@ -481,7 +481,6 @@ files:
481
481
  - lib/view_component/request_details.rb
482
482
  - lib/view_component/slot.rb
483
483
  - lib/view_component/slotable.rb
484
- - lib/view_component/slotable_default.rb
485
484
  - lib/view_component/system_test_case.rb
486
485
  - lib/view_component/system_test_helpers.rb
487
486
  - lib/view_component/template.rb
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ViewComponent
4
- # CaptureCompatibility is a module that patches #capture to fix issues
5
- # related to ViewComponent and functionality that relies on `capture`
6
- # like forms, capture itself, turbo frames, etc.
7
- #
8
- # This underlying incompatibility with ViewComponent and capture is
9
- # that several features like forms keep a reference to the primary
10
- # `ActionView::Base` instance which has its own @output_buffer. When
11
- # `#capture` is called on the original `ActionView::Base` instance while
12
- # evaluating a block from a ViewComponent the @output_buffer is overridden
13
- # in the ActionView::Base instance, and *not* the component. This results
14
- # in a double render due to `#capture` implementation details.
15
- #
16
- # To resolve the issue, we override `#capture` so that we can delegate
17
- # the `capture` logic to the ViewComponent that created the block.
18
- module CaptureCompatibility
19
- def self.included(base)
20
- return if base < InstanceMethods
21
-
22
- base.class_eval do
23
- alias_method :original_capture, :capture
24
- end
25
-
26
- base.prepend(InstanceMethods)
27
- end
28
-
29
- module InstanceMethods
30
- def capture(*args, &block)
31
- # Handle blocks that originate from C code and raise, such as `&:method`
32
- return original_capture(*args, &block) if block.source_location.nil?
33
-
34
- block_context = block.binding.receiver
35
-
36
- if block_context != self && block_context.class < ActionView::Base
37
- block_context.original_capture(*args, &block)
38
- else
39
- original_capture(*args, &block)
40
- end
41
- end
42
- end
43
- end
44
- end
@@ -1,20 +0,0 @@
1
- module ViewComponent
2
- module SlotableDefault
3
- def get_slot(slot_name)
4
- @__vc_set_slots ||= {}
5
-
6
- return super unless !@__vc_set_slots[slot_name] && (default_method = registered_slots[slot_name][:default_method])
7
-
8
- renderable_value = send(default_method)
9
- slot = Slot.new(self)
10
-
11
- if renderable_value.respond_to?(:render_in)
12
- slot.__vc_component_instance = renderable_value
13
- else
14
- slot.__vc_content = renderable_value
15
- end
16
-
17
- slot
18
- end
19
- end
20
- end