Skip to content

Commit c145dc8

Browse files
Remove dependency on ActionView::Base, eliminating the need for capture compatibility patch. (#2287)
* Remove dependency on ActionView::Base, eliminating the need for capture compatibility patch. Co-authored-by: Cameron Dutro <camertron@gmail.com> * Use later version of Rails 7.1 for PVC integration tests * docs updates --------- Co-authored-by: Cameron Dutro <camertron@gmail.com>
1 parent f3090b6 commit c145dc8

File tree

19 files changed

+85
-232
lines changed

19 files changed

+85
-232
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,42 +23,20 @@ jobs:
2323
bundle exec appraisal rails-8.0 rake partial_benchmark
2424
bundle exec appraisal rails-8.0 rake translatable_benchmark
2525
test:
26-
name: test (${{ matrix.rails_version }}, ${{ matrix.ruby_version }}, ${{ matrix.mode }})
26+
name: test (Rails ${{ matrix.rails_version }}, Ruby ${{ matrix.ruby_version }})
2727
runs-on: ubuntu-latest
2828
strategy:
2929
fail-fast: false
3030
matrix:
3131
include:
3232
- ruby_version: "3.2"
3333
rails_version: "7.1"
34-
mode: "capture_patch_enabled"
35-
- ruby_version: "3.2"
36-
rails_version: "7.1"
37-
mode: "capture_patch_disabled"
38-
- ruby_version: "3.3"
39-
rails_version: "7.2"
40-
mode: "capture_patch_disabled"
4134
- ruby_version: "3.3"
4235
rails_version: "7.2"
43-
mode: "capture_patch_enabled"
44-
- ruby_version: "3.3"
45-
rails_version: "8.0"
46-
mode: "capture_patch_disabled"
47-
- ruby_version: "3.3"
48-
rails_version: "8.0"
49-
mode: "capture_patch_enabled"
50-
- ruby_version: "3.4"
51-
rails_version: "8.0"
52-
mode: "capture_patch_disabled"
5336
- ruby_version: "3.4"
5437
rails_version: "8.0"
55-
mode: "capture_patch_enabled"
56-
- ruby_version: "head"
57-
rails_version: "main"
58-
mode: "capture_patch_disabled"
5938
- ruby_version: "head"
6039
rails_version: "main"
61-
mode: "capture_patch_enabled"
6240
env:
6341
BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails_version }}.gemfile
6442
steps:
@@ -76,7 +54,6 @@ jobs:
7654
RAISE_ON_WARNING: 1
7755
RAILS_VERSION: ${{ matrix.rails_version }}
7856
RUBY_VERSION: ${{ matrix.ruby_version }}
79-
CAPTURE_PATCH_ENABLED: ${{ matrix.mode == 'capture_patch_enabled' && 'true' || 'false' }}
8057
- name: Upload coverage results
8158
uses: actions/upload-artifact@v4.4.0
8259
if: always()
@@ -114,7 +91,7 @@ jobs:
11491
bundle && bundle exec rake
11592
env:
11693
VIEW_COMPONENT_PATH: ../view_component
117-
RAILS_VERSION: '7.1.1'
94+
RAILS_VERSION: '7.1.5'
11895
PARALLEL_WORKERS: '1'
11996
coverage:
12097
needs: test

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ nav_order: 6
1010

1111
## main
1212

13+
* BREAKING: Remove dependency on `ActionView::Base`, eliminating the need for capture compatibility patch.
14+
15+
*Cameron Dutro*
16+
1317
## 4.0.0.alpha2
1418

1519
* Add `#current_template` accessor and `Template#path` for diagnostic usage.

docs/api.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,6 @@ so helpers, etc work as expected.
142142

143143
## Configuration
144144

145-
### `.capture_compatibility_patch_enabled`
146-
147-
Enables the experimental capture compatibility patch that makes ViewComponent
148-
compatible with forms, capture, and other built-ins.
149-
previews.
150-
Defaults to `false`.
151-
152145
### `.component_parent_class`
153146

154147
The parent class from which generated components will inherit.

docs/known_issues.md

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,6 @@ en:
2727
2828
It would be lovely if we could support rendering ViewComponents in Jekyll, as it would enable the reuse of ViewComponents across static and dynamic (Rails-based) sites.
2929
30-
## Issues resolved by the optional capture compatibility patch
31-
32-
If you're experiencing issues with duplicated content or malformed HTML output (such as using `concat` in a helper), the capture compatibility patch may resolve these.
33-
34-
[Set `config.view_component.capture_compatibility_patch_enabled` to `true`](https://viewcomponent.org/api.html#capture_compatibility_patch_enabled) to resolve these issues.
35-
36-
These issues arise because the related features/methods keep a reference to the
37-
primary `ActionView::Base` instance, which has its own `@output_buffer`. When
38-
`#capture` is called on the original `ActionView::Base` instance while
39-
evaluating a block from a ViewComponent, the `@output_buffer` is overridden in
40-
the `ActionView::Base` instance, and *not* the component. This results in a
41-
double render due to `#capture` implementation details.
42-
43-
To resolve the issue, we override `#capture` so that we can delegate the
44-
`capture` logic to the ViewComponent that created the block.
45-
46-
### turbo_frame_tag double rendering or scrambled HTML structure
47-
48-
When using `turbo_frame_tag` inside a ViewComponent, the template may be rendered twice. See [https://github.com/github/view_component/issues/1099](https://github.com/github/view_component/issues/1099).
49-
50-
As a workaround, use `tag.turbo_frame` instead of `turbo_frame_tag`.
51-
52-
Note: For the same functionality as `turbo_frame_tag(my_model)`, use `tag.turbo_frame(id: dom_id(my_model))`.
53-
54-
### Compatibility with Rails form helpers
55-
56-
ViewComponent [isn't compatible](https://github.com/viewcomponent/view_component/issues/241) with `form_for` helpers by default.
57-
58-
Passing a form object (often `f`) to a ViewComponent works for simple cases like `f.text_field :name`. Content may be ill-ordered or duplicated in complex cases, such as passing blocks to form helpers or when nesting components.
59-
60-
Some workarounds include:
61-
62-
- Experimental: Enable the capture compatibility patch with `config.view_component.capture_compatibility_patch_enabled = true`.
63-
- Render an entire form within a single ViewComponent.
64-
- Render a [partial](https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials) within the ViewComponent which includes the form.
65-
- Use a [custom `FormBuilder`](https://guides.rubyonrails.org/form_helpers.html#customizing-form-builders) to create reusable form components:
66-
- Using FormBuilder with [Action View helpers](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html).
67-
- Using a FormBuilder overriding all field helpers to render a ViewComponent so each field can be customized individually (for example, [view_component-form](https://github.com/pantographe/view_component-form)).
68-
- Using a lightweight re-implementation of ViewComponent. For example, [Primer ViewComponents](https://github.com/primer/view_components) implemented [`ActsAsComponent`](https://github.com/primer/view_components/blob/main/lib/primer/forms/acts_as_component.rb) which is used in the context of `FormBuilder`.
69-
7030
## Forms don't use the default `FormBuilder`
7131

7232
Calls to form helpers such as `form_with` in ViewComponents [don't use the default form builder](https://github.com/viewcomponent/view_component/pull/1090#issue-753331927). This is by design, as it allows global state to change the rendered output of a component. Instead, consider passing a form builder into form helpers via the `builder` argument:

lib/view_component/base.rb

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,21 @@
1818
require "view_component/with_content_helper"
1919
require "view_component/use_helpers"
2020

21+
module ActionView
22+
class OutputBuffer
23+
def with_buffer(buf = nil)
24+
new_buffer = buf || +""
25+
old_buffer, @raw_buffer = @raw_buffer, new_buffer
26+
yield
27+
new_buffer
28+
ensure
29+
@raw_buffer = old_buffer
30+
end
31+
end
32+
end
33+
2134
module ViewComponent
22-
class Base < ActionView::Base
35+
class Base
2336
class << self
2437
delegate(*ViewComponent::Config.defaults.keys, to: :config)
2538

@@ -35,6 +48,10 @@ def config
3548
end
3649
end
3750

51+
include ActionView::Helpers
52+
include ERB::Escape
53+
include ActiveSupport::CoreExt::ERBUtil
54+
3855
include ViewComponent::InlineTemplate
3956
include ViewComponent::UseHelpers
4057
include ViewComponent::Slotable
@@ -45,6 +62,9 @@ def config
4562
# For CSRF authenticity tokens in forms
4663
delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
4764

65+
# HTML construction methods
66+
delegate :output_buffer, :lookup_context, :view_renderer, :view_flow, to: :helpers
67+
4868
# For Content Security Policy nonces
4969
delegate :content_security_policy_nonce, to: :helpers
5070

@@ -61,7 +81,7 @@ def config
6181
# @param view_context [ActionView::Base] The original view context.
6282
# @return [void]
6383
def set_original_view_context(view_context)
64-
self.__vc_original_view_context = view_context
84+
# noop
6585
end
6686

6787
using RequestDetails
@@ -80,7 +100,7 @@ def render_in(view_context, &block)
80100
@view_context = view_context
81101
self.__vc_original_view_context ||= view_context
82102

83-
@output_buffer = ActionView::OutputBuffer.new
103+
@output_buffer = view_context.output_buffer
84104

85105
@lookup_context ||= view_context.lookup_context
86106

@@ -107,14 +127,20 @@ def render_in(view_context, &block)
107127
before_render
108128

109129
if render?
110-
rendered_template = render_template_for(@__vc_requested_details).to_s
130+
value = nil
131+
132+
@output_buffer.with_buffer do
133+
rendered_template = render_template_for(@__vc_requested_details).to_s
111134

112-
# Avoid allocating new string when output_preamble and output_postamble are blank
113-
if output_preamble.blank? && output_postamble.blank?
114-
rendered_template
115-
else
116-
safe_output_preamble + rendered_template + safe_output_postamble
135+
# Avoid allocating new string when output_preamble and output_postamble are blank
136+
value = if output_preamble.blank? && output_postamble.blank?
137+
rendered_template
138+
else
139+
safe_output_preamble + rendered_template + safe_output_postamble
140+
end
117141
end
142+
143+
value
118144
else
119145
""
120146
end
@@ -206,7 +232,7 @@ def initialize(*)
206232
def render(options = {}, args = {}, &block)
207233
if options.respond_to?(:set_original_view_context)
208234
options.set_original_view_context(self.__vc_original_view_context)
209-
super
235+
@view_context.render(options, args, &block)
210236
else
211237
__vc_original_view_context.render(options, args, &block)
212238
end

lib/view_component/capture_compatibility.rb

Lines changed: 0 additions & 44 deletions
This file was deleted.

lib/view_component/collection.rb

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,8 @@ class Collection
99

1010
delegate :size, to: :@collection
1111

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-
1812
def render_in(view_context, &block)
1913
components.map do |component|
20-
component.set_original_view_context(__vc_original_view_context)
2114
component.render_in(view_context, &block)
2215
end.join(rendered_spacer(view_context)).html_safe
2316
end
@@ -67,7 +60,6 @@ def component_options(item, iterator)
6760

6861
def rendered_spacer(view_context)
6962
if @spacer_component
70-
@spacer_component.set_original_view_context(__vc_original_view_context)
7163
@spacer_component.render_in(view_context)
7264
else
7365
""

lib/view_component/config.rb

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ def defaults
2121
show_previews: Rails.env.development? || Rails.env.test?,
2222
preview_paths: default_preview_paths,
2323
test_controller: "ApplicationController",
24-
default_preview_layout: nil,
25-
capture_compatibility_patch_enabled: false
24+
default_preview_layout: nil
2625
})
2726
end
2827

@@ -145,13 +144,6 @@ def defaults
145144
# previews.
146145
# Defaults to `nil`. If this is falsy, `"component_preview"` is used.
147146

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-
155147
def default_preview_paths
156148
(default_rails_preview_paths + default_rails_engines_preview_paths).uniq
157149
end

lib/view_component/engine.rb

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,6 @@ class Engine < Rails::Engine # :nodoc:
5151
end
5252
end
5353

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-
6054
initializer "view_component.set_autoload_paths" do |app|
6155
options = app.config.view_component
6256

@@ -66,6 +60,39 @@ class Engine < Rails::Engine # :nodoc:
6660
end
6761
end
6862

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+
6996
initializer "view_component.eager_load_actions" do
7097
ActiveSupport.on_load(:after_initialize) do
7198
ViewComponent::Base.descendants.each(&:__vc_compile) if Rails.application.config.eager_load

lib/view_component/slot.rb

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,7 @@ def to_s
5858
if defined?(@__vc_content_block)
5959
# render_in is faster than `parent.render`
6060
@__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)
7062
end
7163
else
7264
@__vc_component_instance.render_in(view_context)

0 commit comments

Comments
 (0)