A web app I'm writing in Ruby on Rails has content panels that are used a lot. I'm attempting to write helpers to render these panels with a little cleaner syntax. I'm not very familiar with writing view helpers, so this may be simple. However, when trying to use the "html" (erb code) passed to a helper in a block, I get weird results where the block is rendered twice.
The relevant code is below. This example is using ERB because I simplified it to avoid any possible issues with HAML, but I was getting the same result with HAML.
The HTML that I want to be rendered is:
<section class="panel panel-default">
<header class="panel-heading">Contacts</header>
<div class="panel-body">
<p>Test</p>
</div>
</section>
This is my helper:
module ApplicationHelper
def panel(type = :default, &block)
tag = content_tag(:section, class: "panel panel-#{type.to_s}") do
block.call PanelHelper.new
end
tag
end
class PanelHelper
include ActionView::Helpers::TagHelper
include ActionView::Context
include ActionView::Helpers::CaptureHelper
def header(text = nil, &block)
if block_given?
tag = content_tag(:header, block.call, class: 'panel-heading')
elsif text
tag = content_tag(:header, text, class: 'panel-heading')
else
raise ArgumentError, 'Either a block must be given, or a value must be provided for the text parameter.'
end
tag
end
def body(&block)
content_tag(:div, block.call, class: 'panel-body')
end
end
end
This is my view:
<%= panel :default do |p| %>
<%= p.header 'Contacts' %>
<%= p.body do %>
<p>Test</p>
<% end %>
<% end %>
This is the HTML that is rendered:
<section class="panel panel-default">
<header class="panel-heading">Contacts</header>
<p>Test</p>
<div class="panel-body">
<p>Test</p>
</div>
</section>
Any ideas why this is happening and how to fix it? I'm probably just misunderstanding something about the view blocks.
Thanks
EDIT
I am able to get it functioning by using this body method:
def body(&block)
@helper.concat tag(:div, class: 'panel-body').html_safe
block.call
@helper.concat '</div>'.html_safe
end
where @helper is the passed in the PanelHelper initializer as self from the main helper module ApplicationHelper. Also I remove the = when calling p.body because we're writing directly to the buffer.
<%= p.body do %>
<p>Test</p>
<% end %>
So, <p>Test</p>
is appearing twice (kind of) because the first instance is the result of calling yield
(or in your case block.call
) inside of the body
code of the ApplicationModule helper. According to this railscast, blocks in the view work differently than normal blocks, in that yield auto inserts the result of block call into the HTML (I'm still not sure why, but I'm trying to figure it out).
But this can be demonstrated by putting nil
at the end of the body
function:
def body(&block)
content_tag(:div, block.call, class: 'panel-body')
nil
end
will result in <p>Test</p>
being placed in the code (the result of block.call
, but not the content_tag call).
However, changing block.call
to "<p>Test</p>"
def body(&block)
content_tag(:div, "<p>Test</p>", class: 'panel-body')
nil
end
will result in nothing being placed in your HTML. So it's the yield/block.call
in a view helper which is having some unexpected consequences. So that is essentially why your seeing <p>Test</p>
twice.
The solution, you can do what @PrakashMurthy suggested and pass the block to the content_tag like so
def body(&block)
content_tag :div, class: 'panel-body' do
block.call
end
end
This works because your helper is not yielding to the block, instead it's passing it onto a method that doesn't have the same behavior of yield
inserting the code into the template.
You can also use the capture
method, which takes the results of the block and returns it as a string.
content_tag(:div, capture(&block), class: 'panel-body')
Using
<% p.body do %>
<p>Test</p>
<% end %>
i.e. <% p.body do %>
instead of <%= p.body %>
would suppress the first <p>Test</p>
in the view.
def body(&block)
content_tag :div, class: 'panel-body' do
block.call
end
end
<%= p.body do %>
<p>Test</p>
<% end %>
would give the output you want.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With