Fix rendering of components that utilize capture
in a parent view context
#240
Conversation
@@ -4,6 +4,16 @@ | |||
module ActionView | |||
module Component | |||
module RenderMonkeyPatch # :nodoc: | |||
attr_accessor :child_component |
I also ran into this, any chance of getting this reviewed/merged? |
@thomasbrus I'm not comfortable with this solution, as it depends on a Rails monkey patch. We've already disabled the existing monkey patches when the library is used with Rails 6.1, and don't plan to re-introduce them. That being said, we do need to address this bug. It just might require making changes to Rails itself. |
Any workarounds for this? |
@thelucid not that I'm aware of. If you find a solution that works for you, let us know! |
@joelhawksley I ended up passing the template instance into the constructor and running any renders and helper calls on that instance. Seems to be working quite well. To clarify, I have a form component that takes a the template as an initializer argument. I store that in an instance variable and renter anything else, including form fields via that instance. |
On further investigation, it seems that if this method was changed to always use the view context, this wouldn't be a problem. To give some context, I'm currently passing a template around to avoid this issue: module ComponentHelper
# A simple wrapper around `form_with`, for FormComponent.
#
def component_form_with(**options, &block)
form_with(local: true, **options) do |form|
FormComponent.new(form, self).then do |component|
yield component
render(component, &block)
end
end
end
end
class FormComponent < ViewComponent::Base
def initialize(builder, template)
@builder = builder
@template = template
end
def fields(**options, &block)
@template.render(Form::FieldsComponent.new(@builder, **options), &block)
end
end I'm happy to create a pull request, but am unsure of any knock on effects of always rendering via the view context. |
It seems I can get around this by manually rendering via the module ComponentHelper
def component_form_with(**options, &block)
form_with(local: true, **options) do |form|
render(FormComponent.new(form), &block)
end
end
class FormComponent < ViewComponent::Base
def initialize(builder)
@builder = builder
end
def fields(**options, &block)
view_context.render(Form::FieldsComponent.new(@builder, **options), &block)
end
end |
@BlakeWilliams what do you think about always rendering in |
I'd need to jump back in and regain context, but if we always use I can update this branch real quick to try that and see if the tests still pass. |
5108638
to
2d7a102
I just pushed up a rebased version with the I think it may be due to the builder created by |
Thinking about it, one possible fix might be to just not use |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
We have the same problem, are there any updates on this? |
@23tux This is something I'll be revisiting soon, so I'm hoping we'll have an update on the status of this problem in the near future. |
526329c
to
f98fb69
Sorry for the delay on this, it's a bit of a tricky problem! I just pushed up a new module that should be includable in components that use a slower, but more correct (from a view component point of view) I haven't had a chance to test this out in a real application yet, but if anyone wants to give it a try and report their experience, that would be awesome. |
f98fb69
to
e2d68b0
222bddc
to
ee9f14d
In Rails, when you create a form it saves a reference to the current template object that is rendering the form. When you render a form builder helper method, like `form.label` inside of a child component the template object will be the original template object from where the form builder was instantiated. This means in cases where we pass a block to form builder helper the `@output_buffer` is the parent component's output buffer. So when that buffer is overridden via `.capture` it's setting the parent component's `@output_buffer` to a new, temporary buffer but it doesn't change the child component that is actually rendering's `@output_buffer`. This means the block is executed in the context of the child components actual `@output_buffer` and immediately writes to the buffer. Since `capture` had nothing written to the temporary buffer, it returns the value of the block call and then inserts that into the parent component's `@output_buffer`. This adds a new module, ActionViewCompatibility which can be included in a component to ensure that any `capture` calls coming from that component will be called in the correct context. This is helpful when using helper methods like `form_for`.
ee9f14d
to
3f993f9
ViewComponent has an issue with rendering partials inside forms see: github/view_component#240. We work around this by adding an in-between partial
ViewComponent has an issue with rendering partials inside forms see: github/view_component#240. We work around this by adding an in-between partial
Unfortunately, not even including |
In Rails, when you create a form it saves a reference to the current
template object that is rendering the form. When you render a form
builder helper method, like
form.label
inside of a child component thetemplate object will be the original template object from where the form
builder was instantiated.
This means in cases where we pass a block to a form builder helper the
@output_buffer
is the parent component's output buffer. So when thatbuffer is overridden via
.capture
it's setting the parent component's@output_buffer
to a new, temporary buffer but it doesn't change thechild component that is actually rendering's
@output_buffer
. Thismeans the block is executed in the context of the child components
actual
@output_buffer
and immediately writes to the buffer. Sincecapture
had nothing written to the temporary buffer, it returns thevalue of the block call and then inserts that into the parent
component's
@output_buffer
.This adds a new module, ActionViewCompatibility which can be included in
a component to ensure that any
capture
calls coming from thatcomponent will be called in the correct context. This is helpful when
using helper methods like
form_for
.