While working on a Rails project recently, I ran into an interesting dilemma when trying to DRY up a big mess of partials that I had accumulated over several months. Below I’ll describe the scenario and show a way to get around some of the limitations in default Rails partial rendering.
You have several partials in your Rails project that are nearly identical, and you’re sick of all this un-DRY code laying around. Some (or all) of these partials can be loaded multiple times on a single page via a collection of objects. Never fear, Rails has a solution for you (sort of)!
Did you know:
Partials can be rendered with a layout? I know, my mind was blown too! The only caveat I’ve found is that the layout file needs to be in the same directory as the partial, as opposed to the layouts directory, though I’m not sure if this is a hard rule or just the default behavior.
<%= render :partial => 'my_partial_file', :layout => 'my_layout_file' %>
Another fun fact, you can call:
<%= render :partial => 'my_partial_file', :collection => @my_collection_of_objects %>
or if the objects in your collection have a class name that matches your partial name:
<%= render @my_collection_of_objects %>
and Rails will render each of those objects into the partial. Supposedly this is faster than looping over the objects and rendering manually, since Rails only has to lookup the partial once. One downside is that you can’t (as far as I can tell) also pass a layout to this method call.
Anyways on to solving the problem at hand…
The most obvious solution to DRY up similar partials is to use the layout parameter and move all your repetitive markup to a layout view. Sometimes things don’t get to be that simple, though. If, for instance, you want to have a named yield inside your partial layout, you’ll quickly find yourself frustrated. Here’s an example:
# _layout.html.erb <p id="special_content"> <%= yield :special_content %> </p> <div id="content"> <%= yield %> </div> # _partial.html.erb <% content_for :special_content, 'This is very special' %> <p>This is not so special</p>
For some reason, named yields inside partial layouts don’t behave correctly. Instead of your expected output, you’ll end up with:
<p id="special_content"> <p>This is not so special</p> </p> <div id="content"> <p>This is not so special</p> </div>
After digging around in the code, I found that you can call
content_for in the place of
yield with only the name argument and achieve similar results, however this has another issue which will require a little backstory to explain.
The content for views is handled by what Rails calls a
ViewFlow. It exposes methods for a
content hash, and
content_for subsequently use those methods to access and populate the content. However when you access that content, it’s not destructive; it never removes the content from that hash. This causes a problem if you’re using
content_for in a loop, because every call you’re just tacking more content onto the same hash key.
What you really need for the above scenario is a way to, when accessing the content from the
ViewFlow, remove it from that hash so you have a clean slate on the next iteration. This is actually trivial to accomplish because the
ViewFlow, and therefore it’s
content hash, are available in the view. Just add a method like this to your
ApplicationHelper and use it in place of your normal named yields:
def yield_content!(content_key) view_flow.content.delete(content_key) end
Now when looping through your collection and rendering your partials, your content should render as expected. My next goal is to get layouts behaving with rendering partials using the
:collection param, though I think I may have different behavior expectations than the Rails designers do on that topic.