views:

131

answers:

2

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?

A: 

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
Jesper Rønn-Jensen
Any time you have to move more than the slightest amount of HTML from the templates into the helpers or controllers,the design starts to seem inside-out. I suspect there's a better design available, and quite possibly with less work.
Wayne Conrad
I accepted this as the answer to the particular question, as it relates to implementing a block and params for it. However, consider Wayne Conrads answer if you implement a similar situation to the one I describe :)
Jesper Rønn-Jensen
+1  A: 

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.

Wayne Conrad
After looking at the development of my code and considering your answer, I finally decided to accept my own answer, as it's closest to the question. However, implementing a partial template in stead of a helper is most likely a better strategy and also gives cleaner code, as you mention.
Jesper Rønn-Jensen