-
Notifications
You must be signed in to change notification settings - Fork 444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use a global output buffer #1307
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a few comments, but otherwise this LGTM!
280734e
to
fc30ff1
Compare
d642187
to
8815444
Compare
1 file(s) had their final line ending fixed:
|
Unfortunately, this branch does not work with our use case, while the original PR, #974, does work fine. I have a simple test case in our application that was used to test the "fixed" behaviour to ensure the ViewComponent's would render correctly. This test case can be reduced to this simple reproduction: # A helper method in our application
def my_helper_method(options, &block)
content_tag(:dig, options, &block)
end <%# Inside a ViewComponent %>
<%= my_helper_method({ foo: :bar }) do %>
<p>Content inside helper method</p>
<% end %> Unfortunately, the content does not render properly with this branch:
|
Thanks for reporting this @percyhanna :) The problem you're running into is caused by the fact that view components are their own instances of View components have always had a sort of weird relationship with helpers. Normally, all the helpers in your app are (as far as I know) automatically included into the module that houses your compiled templates and therefore directly available inside your views. Historically however view components have favored defining methods on component classes themselves instead of relying on the application-wide grab bag of helper methods, which can not only override each other in unintuitive ways but also increase coupling between the component and Action View. So, given all that, I'd say the issue is more related to helpers than it is to the global output buffer, although the two are certainly related. As things stand, you have three options:
Yes, that's right. We're considering including all helpers in view components by default in v3 😄 |
I think this is a valid edge case that we may have to handle on our end. I generally recommend using When it comes to V3 and including all helpers by default, I think it's relatively likely we'd end up with something like: def method_missing(method, *args, **kwargs)
return super unless helpers.respond_to?(method)
helpers.send(method, *args, **kwargs)
end The reason I think that approach is likely is due to the memoization/ivar problem mentioned above. This PR that adds the original view context has more details on the impacts of using/not using the default helpers instance. If we were to include all helpers on ViewComponent's themselves, each component would have its own ivars (and possible collisions, but that's another issue entirely). The "single renderer object" (AKA one object that inherits from All that being said, I think the example provided is something that we have to expect users of the library to run into and expect to work. Before we commit to anything specific, I think we should verify the helper approach and global buffer approach we land on are compatible with one another. |
Hmm ok thanks for that additional context @BlakeWilliams. Let me see if I can get something working. |
…t into output_buffer_stack
Hey everyone, I ended up having to monkeypatch ActionView, but helpers should be working now. Can you confirm @percyhanna? |
e5fddd4
to
60188b9
Compare
Consensus on the team is to ship this as-is for now, then look into a more maintainable approach that doesn't monkeypatch ActionView. The implementation will likely come in the form of an upstream change to Rails (maybe a config option?) and a corresponding update to view_component. Enabling the global output buffer by default is now a stretch goal for v3. If we aren't confident when v3 release time rolls around, we'll punt it to v4. |
Thanks for the thoughts and context for the changes. I can appreciate that having to reach inside
I would assume the same. While some of our helper methods could probably be either converted into their own view components, others really are just basic helper methods, so being able to use them within normal Rails views as well as view components would be ideal. For now, the solution I shared in the other PR is working for us. |
@percyhanna it may not be ideal, but as @BlakeWilliams mentioned it's still important to address because we will continue to delegate to the original view context for helpers in v3 anyway. The Would you be able to test this branch to see if it works for you? I'm planning on merging it on Monday. |
@camertron Unfortunately it looks like this branch still doesn't work with our use case. I tried calling our helper method directly in the component partial, e.g. def method_missing(method, *args, **kwargs, &block)
if helpers.respond_to?(method)
helpers.send(method, *args, **kwargs, &block)
else
super
end
end From some quick testing, I think that the The theory behind my "hack" code above was that it would execute the helper method from within the context of the view component, so that calls to lower-level rendering helpers like |
@percyhanna hmm interesting. Just to rule this out, did you set |
@camertron Sorry, my bad. I think I missed that step in the comments. I enabled that setting, and it has fixed the regression spec I had created, however, in the application I am now presented with an even worse issue. The symptoms appear to be that some helper methods are getting double-escaped, even in our non-ViewComponent partials. I'm not sure if this is strictly a bug in this PR, or some kind of conflict with some other helpers we are using. The most trivial example is this:
<%= javascript_tag do %>
alert('hello');
<% end %> This file produces the following output: <script>
//<![CDATA[
alert('hello');
//]]>
</script> |
Another example, using the
<%= link_to("/some_url") do %>
<i class="fa-regular fa-bullseye"></i><span>Content</span>
<% end %> Produces: <a href="/some_url">
<i class="fa-regular fa-bullseye"></i><span>Content</span>
</a> |
2 file(s) had their final line ending fixed:
|
I just wanted to follow up to confirm what I was seeing: the double-escaped HTML was coming from normal Rails views/partials, not a ViewComponent's views. I didn't try this out on a brand new Rails app without anything else, so it's entirely possible that some other part of our application is interfering with the rendering/escaping. |
Awesome, thanks @percyhanna! At first I couldn't reproduce, but then I tried using I have since fixed the problem and added a test. Can you confirm things are working in your app? |
Awesome! My regression test case passes, and the double-escaping issue has been resolved. Thanks! 🎉 |
Summary
The Problem
Since the beginning, ViewComponent has striven for deep integration with ActionView. One of the known issues however has been using Rails' form helpers with components. Consider this template for the
LabelComponent
:Now let's render it from the
FormForComponent
:The latest version of ViewComponent produces the following HTML:
Notice that the
<label>
tag appears below the inputs even though it's supposed to appear above them. This happens because components render to their own output buffer while the form helpers (and indeed the rest of ActionView) render to another one.Potential Solution
The solution proposed in this PR is to use a single, global output buffer. For now it continues to make sense for ViewComponents to render into their own buffer (mostly because they inherit from
ActionView::Base
). Accordingly I've introduced a data structure called theOutputBufferStack
that is used in place of the usualActionView::OutputBuffer
. Components push newOutputBuffer
s onto the stack whenever they are rendered. The buffer at the top of the stack is the one allActionView::Base
s write to, making it appear as if there is only one, global buffer. Instances ofOutputBufferStack
behave exactly likeOutputBuffer
instances, meaning they can be swapped in and out without any changes to ActionView. Moreover, only component classes need this behavior, so no Rails monkeypatches are required.The global output buffer behavior is opt-in and available either by manually
prepend
ing theViewComponent::GlobalOutputBuffer
module into individual component classes, or globally by setting theconfig.view_component.use_global_output_buffer
Rails configuration setting.With the global output buffer enabled, the following HTML is produced for the example above:
It works! 🎉
Other Information
Unfortunately managing the global output buffer adds some overhead. My benchmarks show that it decreases performance by about 18%. While not ideal, I've tried to mitigate the performance hit by submitting this PR in tandem, which increases overall performance by about 15%. Here are the raw numbers for just this PR (run
bundle exec ruby performance/global_output_buffer_benchmark.rb
):We hope to fully integrate the global output buffer in ViewComponent v3, which will mean merging all the code from
ViewComponent::GlobalOutputBuffer
intoViewComponent::Base
. Doing so should result in fewer method calls and get us back that remaining 3% of performance.