Hide and Go Seek, and Stay DRY (Part 1. Views)

Tasker_ Dashboard.jpg

So, one of our UI specialists does a refresh on a project I work on -- an internal management tool. The redesign is beautiful, but the designer has put in a number of hide-and-reveal toggle buttons into the design. My goal is to make them all work. Preferably from a common mechanism.

The buttons got added to several different controllers. In some cases, the area being hidden is the detail view of one of many items in a list. In other cases, the area is one of several reports on the same page, and is not as tightly tied to a specific object.

Step one is to make it work for a single controller. Step two is to make it work for a second controller, generalizing the code as I go. In this post, I'll show how I made the view code nice and DRY, the next post will cover the controller code.

As initially added, the headers with the hide-and-seek toggle button contained the following HTML (irrelevant details omitted...)

<h2><img src="/images/btn_collapse.gif" id="<%= h project.longname %>_img"
alt="collapse this project" width="15" height="14" class="btn_expand_collapse" />
<%= h project.longname %> Project
</h2>
<%= render :partial => 'project_data %>

The first thing I did was convert the header items to Rails helpers. These three methods build on each other to create the image, the remote link (which was not in the original tag), and the header. I also threw a span to surround the whole thing.

def collapse_expand_button(is_collapse, object, caption)
file = if is_collapse then "collapse" else "expand" end
image_tag "/images/btn_#{file}.gif", :id => dom_id(object, :img),
:alt => "#{file} #{caption}", :width => 15, :height => 14,
:class => "btn_expand_collapse"
end

def collapse_expand_link(is_collapse, object, caption)
action = if is_collapse then :hide else :show end
link_to_remote collapse_expand_button(is_collapse, object, caption),
:url => {:action => action, :id => object}
end

def collapse_expand_content(is_collapse, object, caption)
content_tag(:h2, collapse_expand_link(is_collapse, object, caption) +
hide_reveal_caption(object))
end

def collapse_expand_header(is_collapse, object, caption)
content_tag(:span, collapse_expand_content(is_collapse, object, caption),
:id => dom_id(object, :display))
end

At this point, the view code looks like this:

<%= collapse_expand_header(true, project, "#{h project.longname} Project"%) %>
<%= render :partial => 'project_data %>

An improvement, but we can still clean it up a bit. First up is that caption. Let's move it into the helper for that controller:

def projects_display_caption(project)
"#{h project.longname} Project"
end

The name is prefixed with the name of the controller to prevent name crashes when multiple controllers all have display caption helpers, and also as a naming convention that lets me replace all the calls to output the caption with a call to this method:

def hide_reveal_caption(obj)
helper_method_name = :"#{@controller.controller_name}_display_caption"
send(helper_method_name, obj)
end

You can then remove the caption argument from all those helpers above. (This is not quite the order I did this in -- I didn't generalize the helper names until after I worked on adding this feature to the second controller.)

There's some namespace weirdness here -- the calls to hide_reveal_caption will all happen inside a render :update RJS JavaScript block, which means that they will be executed with self in the page render context, not in the controller context (that's how render :update works). As we'll see later, that means that any call to controller data needs to be made outside the render :update block.

<%= collapse_expand_header(true, project)%>
<%= render :partial => 'project_data %>>

That's even better, but there's one more thing that can be done -- the container with the data is always going to be attached to the toggle, so we can combine all this into a single call to a block helper as follows:

<%= collapse_expand_pair(true, project) do%>
<%= render :partial => 'project_data %>
<%= end %>

That's better. The block helper renders the internal partial and wraps it in a div tag with a consistent naming convention so the controller code can find it. Here's the helper code.

def collapse_expand_container(object, tag_options, content)
tag_options.merge!(:id => dom_id(object, :container))
content_tag(:div, content, tag_options)
end

def collapse_expand_pair(is_collapse, object, tag_options = {}, &block)
content = capture(&block)
result = collapse_expand_header(is_collapse, object) +
collapse_expand_container(object, tag_options, content)
concat(result, block.binding)
end

The collapse_expand_pair method evaluates the block into the content string, then calls the header method listed above, pairs it with the container method in this set of code, and concatenates them both into the template stream. The resulting HTML has a header and a content block, both wrapped in nicely named id tags so the controller method can manipulate them.

One other twist before moving on to the controller code. As I mentioned, the blocks to be toggled can either be tied to a specific object or not. In the "not" case, then the object being passed to these helper methods is a string denoting the report or page section. However, the code I've already shown uses the dom_id function to name the parts of the HTML, and if you pass that the String estimate, the resulting ID will be something like display_string_123234, when I'd like it to be display_estimate. So, I replaced all calls to dom_id in the earlier helpers with the following smart_dom_id method, which handles strings more clearly.

def smart_dom_id(object, prefix)
if object.is_a? String
return "#{prefix}_#{object}"
else
dom_id(object, prefix)
end
end

Tune in tomorrow (or maybe the next day) for the controller side of this code.


If you liked this, you might also enjoy my book Professional Ruby on Rails.

Related posts:

  1. Hide, Seek, and Stay Dry, part two: Controllers
  2. RSpec and Rails Custom Form Builders
  3. Pretty Blocks in Rails Views
  4. Down with HTML + Code Markup!
  5. DRYing up Rails Controllers: Polymorphic and Super Controllers

Topics:

Leave a comment

Powered by WP Hashcash

Launch: Pathfinder Newsletter

    Get a monthly update on best practices for delivering successful software.

    Subscribe via email


    Subscribe via RSS      RSS icon

Topics

Search

WordPress

Comments about this site: info@pathf.com