Skip to content
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

Fix rendering of components that utilize capture in a parent view context #240

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

@BlakeWilliams
Copy link
Member

@BlakeWilliams BlakeWilliams commented Mar 2, 2020

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 a 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.

@@ -4,6 +4,16 @@
module ActionView
module Component
module RenderMonkeyPatch # :nodoc:
attr_accessor :child_component

This comment has been minimized.

@BlakeWilliams

BlakeWilliams Mar 2, 2020
Author Member

Maybe a better name would be child_view_context?

@thomasbrus
Copy link

@thomasbrus thomasbrus commented Mar 31, 2020

I also ran into this, any chance of getting this reviewed/merged?

@joelhawksley
Copy link
Contributor

@joelhawksley joelhawksley commented Mar 31, 2020

@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.

@thelucid
Copy link

@thelucid thelucid commented Jun 24, 2020

Any workarounds for this?

@joelhawksley
Copy link
Contributor

@joelhawksley joelhawksley commented Jun 29, 2020

@thelucid not that I'm aware of. If you find a solution that works for you, let us know!

@thelucid
Copy link

@thelucid thelucid commented Jun 29, 2020

@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.

@thelucid
Copy link

@thelucid thelucid commented Jul 1, 2020

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.

@thelucid
Copy link

@thelucid thelucid commented Jul 1, 2020

It seems I can get around this by manually rendering via the view_context, and therefore remove the need to pass around the template. It still feels like the view context should always be used:

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
@joelhawksley
Copy link
Contributor

@joelhawksley joelhawksley commented Jul 1, 2020

@BlakeWilliams what do you think about always rendering in view_context?

@BlakeWilliams
Copy link
Member Author

@BlakeWilliams BlakeWilliams commented Jul 1, 2020

@BlakeWilliams what do you think about always rendering in view_context?

I'd need to jump back in and regain context, but if we always use view_context are we bypassing render_in and always writing directly to the parent renderers @output_buffer? I think there may be some trade-offs, but that may work.

I can update this branch real quick to try that and see if the tests still pass.

@BlakeWilliams BlakeWilliams force-pushed the BlakeWilliams:nested-render branch from 5108638 to 2d7a102 Jul 1, 2020
@BlakeWilliams
Copy link
Member Author

@BlakeWilliams BlakeWilliams commented Jul 1, 2020

I just pushed up a rebased version with the view_context.render change, sadly it looks like the issue still exists.

I think it may be due to the builder created by form_for and friends captures the current rendering context at call time and view_context may be different by the time the label with a block arg is called? It's also possible it's the existing render_in issue. I'd have to dive in more later, but if anything looks off please let me know.

@BlakeWilliams
Copy link
Member Author

@BlakeWilliams BlakeWilliams commented Jul 1, 2020

Thinking about it, one possible fix might be to just not use render_in in production/dev until the @output_buffer issue can be solved. I'm not a huge fan of that, but I think that may unblock things until we can come up with an upstream solution for Rails.

@stale
Copy link

@stale stale bot commented Aug 29, 2020

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.

@stale stale bot added the stale label Aug 29, 2020
@stale stale bot closed this Sep 5, 2020
@joelhawksley joelhawksley reopened this Sep 8, 2020
Base automatically changed from master to main Dec 21, 2020
@23tux
Copy link

@23tux 23tux commented Feb 19, 2021

We have the same problem, are there any updates on this?

@BlakeWilliams
Copy link
Member Author

@BlakeWilliams BlakeWilliams commented Feb 19, 2021

@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.

@BlakeWilliams BlakeWilliams force-pushed the BlakeWilliams:nested-render branch 4 times, most recently from 526329c to f98fb69 Apr 8, 2021
@BlakeWilliams
Copy link
Member Author

@BlakeWilliams BlakeWilliams commented Apr 9, 2021

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) capture behavior when inside form_for and form_with.

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.

test/test_helper.rb Outdated Show resolved Hide resolved
lib/view_component/action_view_compatibility.rb Outdated Show resolved Hide resolved
@BlakeWilliams BlakeWilliams force-pushed the BlakeWilliams:nested-render branch from 222bddc to ee9f14d Apr 9, 2021
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`.
@BlakeWilliams BlakeWilliams force-pushed the BlakeWilliams:nested-render branch from ee9f14d to 3f993f9 Apr 9, 2021
benswannack added a commit to DFE-Digital/apply-for-teacher-training that referenced this pull request May 11, 2021
ViewComponent has an issue with rendering partials inside forms see: github/view_component#240. We work around this by adding an in-between partial
benswannack added a commit to DFE-Digital/apply-for-teacher-training that referenced this pull request May 11, 2021
ViewComponent has an issue with rendering partials inside forms see: github/view_component#240. We work around this by adding an in-between partial
@zernie
Copy link

@zernie zernie commented Jun 1, 2021

Unfortunately, not even including ActionViewCompatibility helps. This is a pretty big issue that stops me from rewriting my forms with view_component

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

7 participants