I am trying to create a set of view helpers for a project I am working in and as they share a lot of common code, for better or worse, I am trying to take an object oriented approach. However, I am having problems when I try to capture and render HTML.
Instead of rendering the HTML within the tag, it will render it before the tag and also render it as a string within the tag.
I want to understand why there are differences in the behaviour and what I can do to fix it (if it is even possible to)
Example 1: working functional approach
I have the following helper:
# application_helper.rb
def my_helper(summary_text, &block)
tag.details do
concat(tag.summary(tag.span(summary_text)))
concat(tag.div(&block))
end
end
In my html.erb file I have:
<%= my_helper('Summary text') do %>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<% end %>
This will render:
<details>
<summary>
<span>
Summary text
</span>
</summary>
<div>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
</details>
Example 2: non-working Object Oriented approach
In my helper file I have defined the class:
# helpers/my_helper.rb
require 'action_view'
class MyHelper
include ActionView::Context
include ActionView::Helpers
attr_reader :summary_text
def initialize(summary_text)
@summary_text = summary_text
end
def render(&block)
tag.details do
concat(tag.summary(tag.span(summary_text)))
concat(tag.div(&block))
end
end
end
And in my application helper I have:
# application_helper.rb
def my_helper(summary_text, &block)
MyHelper.new(summary_text).render(&block)
end
In my html.erb file I have:
<%= my_helper('Summary text') do %>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<% end %>
This will render:
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<details>
<summary>
<span>
Summary text
</span>
</summary>
<div>
<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>
</div>
</details>
I would expect the Object Oriented approach to have the same result as the functional based approach.
I've had a try at debugging this in the Rails code and if I were to have a guess I think it's something to do with the output_buffer
and the OO helper not using the same one as the view, but I am not sure.
What you want to do here is pass the view context into the builder/decorator/presenter/view compont/thingamajigger and call the methods on it instead:
# do not use require - the Rails component are already loaded through railties
# do not call this "Helper" - that will just make things more confusing.
class ListBuilder
attr_reader :summary_text
attr_reader :context
delegate :tag, :concat, to: :context
def initialize(summary_text, context:)
@summary_text = summary_text
@context = context
end
def render(&block)
tag.details do
concat(tag.summary(tag.span(summary_text)))
concat(tag.div(&block))
end
end
end
def list_with_summary(summary_text, &block)
ListBuilder.new(summary_text, context: self).render(&block)
end