HomeRamblings  ⁄  GeneralRuby LanguageSQL

DRYing your Views

Published: July 05, 2008 (over 9 years ago)
Updated: over 2 years ago

Let me start out by saying that I am finally beginning to understand a bit about that magical Ruby block notion and how implementing methods through block passing can really empower you as a Ruby developer. Thanks to, a most excellent Ruby tutorial, I am definitely feeling a good bit more empowered about getting my Views in Rails all DRY’d up. The problem: I wanted to introduce a property editor metaphor for the website I’m working on where properties could expose edit forms for just about any situation, much like wordpress’ widget interface does. That is, there’s an area of the current page that shows basic information about the property in a read-only (and preferably compact) form, and user could click an “edit” button and exposed an editable version of the properties. The “edit” link becomes a “cancel” link, which, when clicked, simply discards the edit form and return the initial compact view state.

Clicking EDIT to extend the display for editing

The problem is, Rails doesn’t really have any good helpers for reusing a design concept in your views. I found that even with partials, the code was really starting to bog down and become both unmaintainable and unreadable. I needed a simple mechanism to construct my property editors. I started out with helper methods. But that wasn’t much better as the code just started ending as ruby here-docs and became a maintenance issue of another sort! Take a look at this embarrassment:

1
2
3
4
5
6
7
8
9
10
11
result = "<table><tr>"
result += "<th colspan=2>"
result += button_image_action('Group selected together', 'link.png')
result += button_image_action('Copy selected to this group', 'link_add.png')
result += button_image_action('Move selected to this group', 'link_go.png')
result += button_image_action('copy group', 'page_copy.png')
result += button_image_action('cut group', 'cut.png')
result += button_image_action('paste group', 'page_paste.png')
result += button_image_action('delete group', 'delete.png')
result += "</th></tr>"
result += "</table>"

Ok, I admit it, I chopped out more than half again as many lines as you’re looking at there, and perhaps worse, this isn’t actually the original HTML code that led me down this path, but lets take a look at this approach anyway, as I learned something valuable in the process and it was my first attempt at DRYing my views. First thing, first, get the above HTML back into the erb file:

1
2
3
4
5
6
7
8
9
10
11
<table>
  <th colspan=2>
    <%= button_image_action('Group selected together', 'link.png') %>
    <%= button_image_action('Copy selected to this group', 'link_add.png') %>
    <%= button_image_action('Move selected to this group', 'link_go.png') %>
    <%= button_image_action('copy group', 'page_copy.png') %>
    <%= button_image_action('cut group', 'cut.png') %>
    <%= button_image_action('paste group', 'page_paste.png') %>
    <%= button_image_action('delete group', 'delete.png') %>
  </th></tr>
</table>

Which promptly let me to trying something like this:

1
2
3
4
5
6
7
8
9
def button_image_action(title, image_filename, button_text = '', url = '')
  form_for @cube url do |f|
    <<-TEXT
      <BUTTON alt='#{title}' title='#{title}' type='submit'>#{button_text}
      <IMG alt='#{title}' title='#{title}' src='/images/#{image_filename}'>
      </BUTTON>
    TEXT
   end
end

I thought it was shaping up nicely, but I got completely blindsided when it came to rendering this bit. You see, the Rails Form Helpers are unavailable in a regular helper! They require ERB (being inside <% %> and <%= %>) to actually properly render their blocks. I fiddled around for a bit to try to jump this hurdle, but it just simply ain’t gonna work without some serious hacking. I’m happy to report that I got a little wiser and stepped back from the problem and reevaluated my approach in general.

I realized, I wasn’t getting away from doing HTML code in helpers! Bottom line: Helpers are great, but you’re doing something clearly against “The Rails Way” when you start rendering large chunks of HTML code from helpers. It just isn’t a great approach and if you have designers on your team, they can get quite lost hunting around for all those HTML snippets. So, how could I get all of my HTML back into *.erb files where they belonged, yet develop some helpers that would make mincemeat of the coding effort to build these fancy toolbars and forms I had coming? Fellow colleague, Steven Beales said, “you need to use Ruby blocks.” To which, my first reaction was, “what are Ruby blocks?” To my great benefit, he explained by pointing me straight at Block Helpers and DRY Views in Rails. Truth be told, I mostly understood this article, but not entirely. Even so, I dutifully set off to solve my problem with this approach. This led me to a bit of copying and pasting (and crediting)…

1
2
3
4
5
6
7
8
9
# From:  http://errtheblog.com/posts/11-block-to-partial
#
# captures the output of our passed block using Rails’ capture method and adds
# it to our options hash under the key of :body. Next it renders our passed
# partial, sending in our options hash as :locals
def block_to_partial(partial_name, options = {}, &block)
  options.merge!(:body => capture(&block))
  concat(render(:partial => partial_name, :locals => options), block.binding)
end

Look at that carefully and, if I may be so bold to suggest, go read up on ERR the Blog to better understand as this method is key and I daresay was appropriately called “tomfoolery” by the wise authors therein. This bit of code really forced me to finally begin to learn and understand what the heck block passing was all about. In a nutshell, this little gem lets you pass options that become local variables to the partial and the block is the contents to be rendered within the partial when you yield to the passed block.

An Aside

There were a couple instances where I was building a repeated elements with pretty much the same resulting HTML (namely toolbar buttons where only the image, caption, alt, title, and onclick events were changing). Taking inspiration from Ilya Grigorik’s rounded box implementation, I felt he had the approach I wanted, but I didn’t always need to pass in blocks for what I was do in every situation. Unfortunately, I didn’t understand blocks! I had to go back to school and understand them in order to adapt his approach to my situation. After schooling myself a bit, I came up with this modified version of the block_to_partials to pass those attributes in (no block necessary!). I named the simplified version “variables_to_partial” as follows:

1
2
3
4
# Calls given partial, injecting the options hash into the partial's local hash
def variables_to_partial(partial_name, options = {})
  render(:partial => partial_name, :locals => options)
end

The block_to_partial is a great start, but I wanted to simplify my calls and also enforce a bit of use-pattern on the developer (i.e. me) so that all property editors had essentially the same behavior pattern (and components). Thus, we introduce the property editor container which in turn can have one or more sections associated with it:

1
2
3
4
5
6
7
# Generates a division around the given block that is styled with "property-editor"
# Property editors are intended as a view on data that has both a condensed display and
# an expanded display that lets you edit the information contained in the property editor.
def property_editor(id, title, options = {}, &block)
  @id = id
  block_to_partial 'layouts/property_editor', options.merge(:id => id, :title => title), &block
end
1
2
3
4
5
6
7
8
9
10
# Renders content of a section (provided by block) inside a property editor division
# The stylesheet contains styling for the following section_id's:
#   => "show" - the section initially visible
#   => "edit" - the section made visible when the edit button is clicked
def property_editor_section(section_id, options = {}, &block)
  block_to_partial('layouts/property_editor_section',
    options.merge(:section_class => "property-editor-#{section_id}",
      :section_id => "#{@id}-#{section_id}"),
      &block)
end

The property editor partial:

1
2
3
4
5
6
7
8
9
<div <%= %[id="#{id}"] %> class="property-editor">
  <div class="property-editor-title">
    <div style="float:right">
      <a href="javascript: void(0)" onclick="toggle_div('<%= id %>-show');toggle_div('<%= id %>-edit');">edit</a>
    </div>
      <%= title %>
  </div>
  <%= body %>
</div>

and the property editor sections are rendered with this partial:

1
2
3
<div <%= %[id="#{section_id}"] %> <%= %[class="#{section_class}"] %> <%= %[style="display:none"] if section_id =~ /(.+)?\-edit/  %>>
  <%= body %>
</div>

With these helpers and partials, property editors as a concept is very easy to pull together in my view files. As you can see, if my section is an “edit” section, its hidden at the outset. The button to toggle between show and edit is in the property editor partial itself in which an onclick javascript calls toggle_div to make this happen. The Javascript is very straightforward:

1
2
3
4
function toggle_div(div_name) {
  var div = $(div_name);
  div.getStyle('display') == 'none' ? div.setStyle('display', 'block') :  div.setStyle('display', 'none');
}

Here’s just one such property editor operating on my “cube model”:

1
2
3
4
5
6
7
8
9
10
<% property_editor 'properties', 'Properties' do %>
  <% property_editor_section 'show' do %>
    <%= @cube.name %><br/>
    <%= render :partial => '/cubes/cube_stats' %>
  <% end %>

  <% property_editor_section 'edit' do %>
    <%= render :partial => 'form' %>
  <% end %>
<% end %>

What’s more, I had a complete set of CSS selectors with which I could control my property editor and its associated sections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.property-editor {
  margin-top: 5px;
  border: 3px solid #FDEFE6;
}

.property-editor-title {
  padding: 2px;
  padding-left: 5px;
  padding-right: 5px;
  background-color: #EEE;
  color: #2F6569;
}

.property-editor-show, .property-editor-edit {
  padding: 5px;
}

.property-editor p {
  margin: 0px;
  padding: 0px;
}

Well, all in all, that’s a good bit of rambling, but I hope others can benefit from my experiences with Ruby, blocks, partials, and DRY up those views.

comments powered by Disqus