- We design and build extraordinary applications for companies looking to make the next great idea a reality.
- learn more
Hide and Go Seek, and Stay DRY (Part 1. Views)
![]()
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.
Topics: Ruby on Rails
Leave a comment
About Pathfinder
Recent
- Rails ThreatDown!
- Automated Deployments Rock
- Bandwidth profiling Flex projects and more with Charles
- iPhone SDK: UIViewController Testing & TDD
- Icons are evil; so are menus - unless you do them right
- The Truth About Designing For Security
- GWT, Gadgets and OpenSocial, Part 2
- Has Many has_many: A Refactoring Story
- The Hidden Power of Canvas
- Review of fixture_replacement2 plugin
Archives
- November 2008
- October 2008
- September 2008
- August 2008
- July 2008
- June 2008
- May 2008
- April 2008
- March 2008
- February 2008
- January 2008
- December 2007
- November 2007
- October 2007
- September 2007
- August 2007
- July 2007
- June 2007
- May 2007
- April 2007
- March 2007
- February 2007
- January 2007
- December 2006
- November 2006
- October 2006
- September 2006
- August 2006
- July 2006
- June 2006
- May 2006
- April 2006
- March 2006

