Recently I was faced with an interesting problem: I wanted to create a modular, portal-like page layout natively in Ruby on Rails without using another layer in the architecture like SiteMesh or ALUI. Java has some pretty mature frameworks for this, like Tapestry, but I found the Ruby on Rails world to be severely lacking in this arena.
I started with Rails 2.0 and the best I could come up with at first brush was to create a html.erb page comprised of several partials. Each partial would basically resemble a "portlet." This works fine, but with one showstopping pitfall -- you can't call controller logic when you call render :partial
. That means in order for each page component (or portlet, if you like) to maintain its modularity, you would have to either 1) put all the logic in the partial view (which violates MVC) or 2) repeat all the logic for each component in the controller for every page (which violates DRY).
If that's not sinking in, let me illustrate with an example. Let's say you have two modular compontents. One displays the word "foo" and the other "bar", which are each contained in page-level variables @foo
and @bar
, respectively. You want to layout a page containing both the "foo" and the "bar" portlets, so you make two partials.
"foo" partial (app/views/test/_foo.html.erb):
<%=@foo%>
"bar" partial (app/views/test/_bar.html.erb):
<%=@bar%>
Now, you make an aggregate page to pull the two partials together.
aggregate page (app/views/test/aggregate.html.erb):
<%=render :partial => 'foo' %>
<%=render :partial => 'bar' %>
You want the resulting output to say "foo bar" but of course it will just throw an error unless you either embed the logic in the view (anti-MVC) or supply some controller logic (anti-DRY):
embedded logic in the view (app/views/test/_foo.html.erb):
<%@foo = 'foo'%>
<%=@foo%>
embedded logic in the view (app/views/test/_bar.html.erb):
<%@bar = 'bar'%>
<%=@bar%>
-- OR --
controller logic (app/controllers/test_controller.rb):
def aggregate
@foo = 'foo'
@bar = 'bar'
end
Neither solution is optimal. Obviously, in this simple example, it doesn't look too bad. But if you have hundreds of lines of controller logic, you certainly don't want to dump that in the partial. At the same time, you don't want to repeat it in every controller that calls the partial -- this is supposed to be modular, right?
What a calamity.
I did some research on this and even read a ten-page whitepaper that left me with no viable solution, but my research did confirm that lots of other people were experiencing the same problem.
So, back to the drawing board. What I needed was a way to completely modularize each partial along with its controller logic, so that it could be reused in the context of any aggregate page without violating MVC or DRY. Enter the embed_action plugin.
This plugin simply allows you to call invoke a modular bit of code in a controller and render its view, but not in a vacuum like render :partial
. With it, I could easily put controller logic where it belongs and be guaranteed that no matter where I invoked the "portlet," it would always render correctly.
Here's the "foo bar" example, implemented with embed_action
.
"foo" controller (app/controllers/foo_controller.rb):
def _foo
@foo = 'foo' #this logic belongs here
end
"bar" controller (app/controllers/bar_controller.rb):
def _bar
@bar = 'bar' #this logic belongs here
end
aggregate view (app/views/test/aggregate.html.erb):
<%= embed_action :controller => 'foo', :action => '_foo' %>
<%= embed_action :controller => 'bar', :action => '_bar' %>
That's it! Note that there is no logic in the aggregate controller -- that's not where it belongs. Instead, the foo and bar logic has been modularized/encapsulated in the foo and bar controllers, respectively, where the logic does belong. Now you can reuse the foo and bar partials anywhere, because they're 100% modular.
Thanks to embed_action
, I was finally able to create a completely modular page (and site) design, with very little effort on my part.
In a follow-up post (Part 2), I'll explain how you can create really nice-looking portlets using everything above plus layouts and content_for
.