Larry is building a personal home page. He’s using Ruby on Rails and fla-fla-flying. Why, at the rate he’s going, link groups and banner exchanges will be begging him to join in no time flat.
Just recently he was Googling and came across a post on Err the Blog titled Content for Whom? While the writer of said blog is a bit of a flake, Larry was able to make his site more dynamic and DRY using content_for after reading the post. Here’s how.
The Sidebar
We all know Rails compiles views before it compiles the layout. That is, if you set @page_title in your view, you may reference it in your layout. Something like <title>My Site :: <%= @page_title %></title>. content_for is no different: it allows you to ‘assign’ a chunk of HTML or Ruby to a symbol which may be referenced in your layout.
Here’s Larry’s layout:<html> <head><title>Larry's Personal Home Page</title></head> <body> <table width="100%"> <tr><td> <%= yield %> </td><td> <%= yield :sidebar %> </td></tr> <tr><td colspan="2" align="center"> Copyright (c) 2006 Larry </td></tr> </table> </body> </html>
The first yield, clearly, will be replaced with Larry’s view when rendered. The second yield is a bit different: he’s passing it a symbol, saying he wants to yield :sidebar at that point in time—whatever :sidebar may be.
Let’s look at Larry’s view:<font color="red">Welcome to my website.</font><br> <img src="under_construction.gif"><br> <font color="blue">It was coded by hand in notepad.</font><br> <font color="green">Tell me what you <blink>think!!</blink></font><br> <% content_for :sidebar do %> Hot Links<br> <a href="http://www.netscape.com">Netscape</a><br> <a href="http://www.lycos.com">Lycos</a><br> <a href="http://www.walmart.com">Wal Mart</a><br> <% end %>
You got it, right? yield :sidebar is going to be replaced by whatever block is passed to content_for :sidebar. In this case, Larry’s sidebar.
Ah, solidarity:
That’s fairly useful, but it means Larry has to set :sidebar in every view if he wants one to always show up. A nice thing to do is provide a default.
The Conditional
Larry just needs to replace, in his layout,<%= yield :sidebar %>
with
<%= (sidebar = yield :sidebar) ? sidebar : render(:partial => 'shared/sidebar') %>
Now shared/sidebar will always be rendered unless a view defines :sidebar with content_for. In those cirumstances the view’s :sidebar will be shown. So, you can override a default element of the layout. Nice.
As an aside, you can access @content_for_sidebar directly but that’s not legit. Deprecated.
The Faux Nested Layout
Imagine Larry has a whole bunch of controllers. He is happy with the default sidebar trick but there’s still an issue: he wants all the HomeController views to have a standard sidebar and all the UsersController to have a different standard sidebar.
One idea is to remove the shared/ from the conditional in his layout:
<%= (sidebar = yield :sidebar) ? sidebar : render(:partial => 'sidebar') %>
Now either home/_sidebar.rhtml or users/_sidebar.rhtml will be rendered depending on which controller you’re in. The problem is Larry loses his ‘catch-all’ default sidebar and is forced to create a _sidebar.rhtml file for every controller. Not cool.
I tell Larry, “Throw away that code and change it back to the conditional which uses shared/sidebar. Now, think about layouts. If you have a HomeController and home.rhtml exists in your views/layouts/ directory, Rails will render that. Perfect place to define a home-wide sidebar, yeah?”
views/layouts/home.rhtml:<% content_for :sidebar do %> Cool Pages on My Site:<br> <a href="/home/lonely">LonelyGrrl Videos</a><br> <a href="/home/pagerank">My Google Pagerank</a><br> <a href="/home/web2">Web 2.0 Logo Generator (lol)</a> <% end %> <%= render :file => 'layouts/application' %>
This ‘layout’ file is setting :sidebar then rendering the application layout. Larry just needs to do the same thing for views/layouts/users.rhtml and he’s all set.
Let’s recap. Larry can now define content_for :sidebar in a view, define it for a whole set of controller views, and also have a site-wide default. All at the same time. Talk about power!
One caveat here is that content_for appends content. It don’t replace it. If you’re defining :sidebar in a view under views/home/, using the above example, you’re going to end up with what looks like two sidebars: the one in the home.rhtml layout file and the one in your specific view.
And hey, if you want real nested layouts there’s a plugin for Ruby on Rails available. So get that.
The Stylesheet
Years pass. Larry learns a bit of CSS and begins working for a big, corporate website. Special promotions are demanded by advertisers: they want to advertise on pages which have their own unique style with which “the kids” can identify. Something street. Like those PSP graffiti ads.
Larry, now a seasoned Rails pro, knows this is not a problem. content_for, after all. He comes up with this:
<html> <head> <title>Big Corporate Website 4.0 Beta</title> <%= stylesheet_link_tag 'main' %> <%= yield :stylesheet %> </head> <body> <%= yield %> </body> </html>
See it? If there’s no content_for :stylesheet, everything works. The yield :sidebar just returns nil. However, Larry can also bust out a content_for :stylesheet in any view to add a bit more spice to that particular page. Like:
<% content_for :stylesheet do %> <%= stylesheet_link_tag 'railsconf' -%> <% end %> <h1>Welcome to our RailsConf information page!</h1>
Now his h1’s can be as nutty as all hell and won’t affect the other h1’s in his meticulously crafted website. He can even change the background color. Something pastel, I’d imagine.
The Fin
All of this content_for magic lovingly wraps ActionView’s capture method. The Rails API has the scoop. Just try to remember :layout is off limits when dealing with content_for. If you use it, you’ll just be messing with what gets displayed in your layout’s vanilla <%= yield %> call. Also heed this warning from the API docs: “Beware that content_for is ignored in caches. So you shouldn’t use it for elements that are going to be fragment cached.”
The web’s really all about HTML, amirite? I dig the stuff Bruce is doing and hey, SimplyHelpful looks like a step in the right direction. My memories of Smarty are cherised, but ActionView has stolen my heart.
Glad to hear Larry learned CSS, those tables for layout in the first example were making me nervous!
(Great tips, too!)
Nicely done.
Hmm, looks like the Textile got a little screwed :(
Good post! Larry is my hero for today.
small comment to Peter’s post. The line should look like: <%= (yield :sidebar) || render(:partial => ‘shared/sidebar’) %>
What if the sidebar content is dynamic though, and needs to read from several different tables (models) regardless of which view is currently being shown. How should this data be prepared for the sidebar?
Jens in this case you put before_filter to controller that needs sidebar data for all (not necessary) actions (views).
Can you clarify this bit for me?
“Just try to remember :layout is off limits when dealing with content_for. If you use it, you’ll just be messing with what gets displayed in your layout’s vanilla <%= yield %>”
How would you go about overriding the layout for a particular method or class then? Say I have an default application layout defined, but also have 3 more layouts which might be used throughout the site, and I am using yield to load up sub layouts which contain the content_for blocks?
Nice post,I was keep looking for a solution to avoid having to add “content_for” in each view for the sidebar before came to your site.
I’wonder if there is any way to keep the sidebar unchanged for each session,that is to say,init only once,of course dynamic content,must I use ajax to render each view to a partical to archive this kind of behavior?
Thanks!
Calling yield on a nil value returns an empty string in rails 2.0, so you need to change the logic in your layouts that call yield to something like this:
<%= :sidebar ? (yield :sidebar) : render(:partial => ‘shared/sidebar’) %>
Thanks for this. Didn’t have a clue ‘til I read this page
Chime in.