We’ve all seen helper methods that take blocks. They’re super useful. Recently, Brandon Keepers wrote about overriding link_to to accept a block. The inimitable Bruce Williams, too, has oft written of helpers that take blocks and given examples on how to further twist them.
There’s one issue I have with these techniques. Bruce is obviously on top of his game, but keeping structural HTML in my helper files seems somewhat strange. Will I remember where it’s stored? How will my CSS-wiz buddy know where to find the divs that are popping up? I’d rather keep my HTML in a partial and somehow write a simple method which renders that partial, along with whatever I pass to it as a block.
So, sidebar modules. That’s a great example. Let’s say we have a standard two column layout and our narrower right column (the “sidebar”) has a uniform way of displaying content. Each module is a div with an h2 header, then the arbitrary content. The content can be the 5 most recent stories, an ad, or maybe even an embedded YouTube video. Whatever. The point is, I want the container (and the h2) to always be the same and be kept in one place.
One approach is to make a sidebar module partial and pass it in the content we want. But this gets awkward fast. What if the content is a bunch of HTML? Do we stick that in a string and then pass the partial a string? Perfect place for a block.
Consider app/views/shared/_sidebar_module.rhtml (kept in shared so we can reference it from any view easily):
<div <%= %[id="#{id}"] unless id.blank? %> class="sidebar_module"> <h2><%= title %></h2> <%= body %> </div>
(Notice we’re checking for the existence of id. It’s not mandatory.)
As it stands, we’d probably have to call this partial in the following way:
<%= render :partial => 'shared/sidebar_module', :locals => { :title => 'Video of the Day', :body => '<div class="video"><crazy youtube embed html></div>', :id => 'video_otd' } %>
Yikes. Wouldn’t it be great if we could call this partial like so:
<% sidebar_module 'Video of the Day', :id => 'video_otd' do %> <div class="video"><crazy youtube embed html></div> <% end %>
Then we’d also be able to do something like:
<% sidebar_module 'Recent Stories' do %> <%= render :partial => 'recent_stories', :collection => @recent_stories %> <% end %>
Thanks to Bruce and Brandon, it’s possible. We can keep the partial where it is and make this work with a few simple helper methods.
First, let’s define the sidebar_module method in our helper file.
def sidebar_module(title, options = {}, &block) block_to_partial 'shared/sidebar_module', options.merge(:title => title), &block end
We’re giving sidebar_module three parameters: title, options, and a block. title is needed. In our video example, it’s obviously Video of the Day. options is an optional hash. It’s what catches :id => ‘video_otd’. And the block is, well, our block.
What we do with these parameters is the interesting part. Right away we call another method: block_to_partial. I’ll explain this one in a second. What we hand block_to_partial is the partial we want to render, our options hash (as you can see we’re adding title to the options hash under the key of :title), and our block. Basically sidebar_module just wraps block_to_partial, giving us the title and letting us omit the partial for convenience’s sake.
Finally, block_to_partial? Simple. Defined in our helper file.
def block_to_partial(partial_name, options = {}, &block) options.merge!(:body => capture(&block)) concat(render(:partial => partial_name, :locals => options), block.binding) end
At the risk of being redundant, this little method is rather simple. It first 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. This, of course, turns :id => ‘video_otd’ into id within our partial, and :title into title, etc. As a result, the block’s value will always be available inside of our partial as body. The rendered partial (a string) is passed to concat with the block’s binding. Bruce’s Helpers that take Blocks post explains this tomfoolery rather well.
And there you have it. Now your CSS dude who is testing the Rails waters can modify partials and play with blocks without having to dig through your helper files. Though he’s probably in there, anyway. Damn CSS dudes.
Update: Fixed some broken links. Thanks Ed!
Love it!
Good to see I’m inimitable these days. ;-)
In my own defense, I throw markup in helpers because I am the CSS guy—but I think the article; a good twist.
Thanks Bruce. I love your technique and there is definitely a place for both the helper.rb and the _partial.rhtml way of implementing it. My hope is that in the future everyone will speak Ruby and this post will be left as a shameful reminder of our HTML trodden past.
You could write it like:
so in partial you could just yield:
Love the blog Just noticed you might want to fix the display in the second part of Maxim Kulkin’s entry (with the %lt;) Looks like you were trying to prevent injection? Anyhow, keep up the good work!
Found this when googling something else. Just thought I’d add: as of [#7261]http://dev.rubyonrails.org/changeset/7261, you can in effect pass blocks to partials.
Hum, that’s cool. How can I capture the “content” which sidebar_module produces with content_for_layout, so that it can be yielded?
This could be usefull if I want to generate a sidebar_module dynamically inside an view..
Is it possible to pass the options from sidebar_module to any partials within sidebar_module as locals?
Chime in.