Ruby using block and params to DRY up template
I have a pattern in a couple of templates that's repeated. The code is for rendering a set of tabs and content for each tab.
Contents of /app/views/general/_product_groupings.html.erb
<table cellpadding="1" cellspacing="0" class="sub_container clear">
<tr>
<% first_visible_tab = true %>
<% @bundle.groupings.each do |group| %>
<td id="tab_heading_for_group_<%= group.id %>" class="tab_heading <%= 'selected' if first_visible_tab %>" onclick="show_tab('product_type_<%= group.id %>')"><%= one_liner(group.name) %></td>
<td></td>
<% first_visible_tab = false %>
<% end %>
<td class="last"></td>
</tr>
<tr>
<td colspan="99" class="tab_content">
<% first_visible_tab = true %>
<%# groupings is an array of products %>
<% @bundle.groupings.each do |group| %>
<div style="display: <%= (first_visible_tab) ? '' : 'none' %>" id="tab_body_for_group_<%= group.id %>" class="tab_body container_inner">
<% first_visible_tab = false %>
<% template = case group.grouping_type
when 'selection'
开发者_开发百科 :product_selection_group
when 'group'
:product_optional_group
when 'product'
:product_optional_group
end %>
<%= render :partial => template.to_s, :locals => {:group => group} %>
</div>
<% end %>
</td>
<tr>
</table>
The code consists of some parts. There is the general parameter passed in, @bundle
. There is a header section (lines 1-10):
<table cellpadding="1" cellspacing="0" class="sub_container clear">
<tr>
<% first_visible_tab = true %>
<% @bundle.groupings.each do |group| %>
<td id="tab_heading_for_group_<%= group.id %>" class="tab_heading <%= 'selected' if first_visible_tab %>" onclick="show_tab('product_type_<%= group.id %>')"><%= one_liner(group.name) %></td>
<td></td>
<% first_visible_tab = false %>
<% end %>
<td class="last"></td>
</tr>
In the header section, there are parts that differ each place code like this is used: The collection iterated upon, @bundle.groupings
. The parameter for show_tab()
onclick. The id of the tab id="tab_heading_for_group_<%= group.id %>"
.
Below the header, there is an area that i reckon could be yielded as a block, the real content of each tab content area (lines 19-27):
<% template = case group.grouping_type
when 'selection'
:product_selection_group
when 'group'
:product_optional_group
when 'product'
:product_optional_group
end %>
<%= render :partial => template.to_s, :locals => {:group => group} %>
Just above the content is the same collection @bundle.groupings.each do ...
repeated from the head section.
I would really much like to DRY this up. By defining a block with the content that can be yielded inside a helper method.
I think this method should take the following params:
- id_template
- onclick_method #perhaps with a pattern for the parameter to the onclick method
- collection
- block to be yielded inside the content area of the tab
Question is, how do I make a helper method to take both block and params?
After reading Coda Hales examples on blocks, I found a solution that worked. The trick is to add your usual parameters, followed by the block like so:
method_call(params) { |x| x.do_stuff_inside_the_methods_context }
And x will be given the scope of the yield
inside your method. Here is the rewrite of the code used in the example. (for now, leaving out any dynamic parameters to the onclick
method)
View:
<% options = {
:iterator => @bundle.groupings,
:id_prefix => "group"
} %>
<%= render_tabs(options) { |product|
template = case product.grouping_type
when 'selection'
:product_selection_group
when 'group'
:product_optional_group
when 'product'
:product_optional_group
end
render :partial => template.to_s, :locals => {:group => product}
} %>
Helper:
def render_tabs(opts, &block)
collection = opts[:iterator]
prefix = opts[:id_prefix]
buf = %(
<table cellpadding="1" cellspacing="0" class="sub_container clear">
<tr>
)
first_visible_tab = true
collection.each do |iter|
classnames = "tab_heading"
classnames << " selected" if first_visible_tab
td_id = "#{prefix}_#{iter.id}"
buf << %(
<td id="tab_heading_for_#{td_id}" class="#{classnames}" onclick="show_tab('#{td_id}')">#{one_liner(iter.name) }</td>
)
first_visible_tab = false
end
buf << %(<td class="last"></td>
</tr>
<tr>
<td colspan="99" class="tab_content">)
first_visible_tab = true
collection.each do |iter|
td_id = "#{prefix}_#{iter.id}"
display_attr = (first_visible_tab) ? '' : ' style="display:none"'
buf << %(
<div #{display_attr} id="tab_body_for_#{td_id}" class="tab_body container_inner">
)
first_visible_tab = false
buf << yield(iter)
buf << %(
</div>
)
end
buf << %(
</td>
<tr>
</table>
)
buf
end
The only thing I'd do here is to get rid of the case statement by putting the template name in the code, perhaps in the helper*, so that the view can just ask someone else which template to use:
<%= render :partial => template_for_group(group), :locals => {:group => group} %>
You don't show us the original templates, but I gather there is common code in them. I'd be inclined to factor that out by moving the common code into its own template(s) and having each template <% render :partial...>
the common bits as needed.
*Although MVC seems to call for the template name to be decided in the view or helper, from an OO point of view it really belongs in group. Pick one. If it hurts, that's the code telling us that the other choice was better.
精彩评论