Rendering a Collection of Heterogeneous Objects with View Resolvers

About a month ago Obie Fernandez showed us how to do this using the new Object#to_partial_path API in Rails 3.2. This feature has been a long time coming and represents a significant improvement over the hacks I’d been using for this sort of thing.

There was one thing that bugged me about his post however. In order to reuse the same templates from within a namespace Obie monkey patched a private ActionView method. In my opinion this is almost always a Bad Idea.

Fortunately Rails 3 exposes a public API which allows you to accomplish the same thing via View Resolvers. This post will show you how.

The Problem

If your view is rendered by a namespaced controller the namespace will be prefixed to all template lookups.

Say we are exposing a list of items posted to our app in an admin view. You might have a controller such as this:

module Admin
  class FeedController < ::ApplicationController
    expose(:items) { Item.all }
  end
end

And you are rendering a collection in app/views/admin/feed/index:

%ul
  = render items

The default lookup would search for an item partial within app/views/admin/items.

We want it to look in app/views/items.

Introducing the View Resolver

To accomplish this without monkey patching add an items_resolver.rb file somewhere that Rails can autoload it. It should look like this:

class ItemsResolver < ::ActionView::FileSystemResolver

  def initialize
    super("app/views")
  end

  def find_templates(name, prefix, partial, details)
    super(name, 'items', partial, details)
  end

end

The key thing to note is the second argument to super. We ignore the prefix and specify our own. This is the bit that lets us avoid monkey patching.

Using the View Resolver

One of the downsides of monkey patching is that changes will affect your entire app. With a view resolver we can be explicit about when to modify our lookup paths.

Modify the admin feed controller to append your custom view resolver like so:

module Admin
  class FeedController < ::ApplicationController
    append_view_path ItemsResolver.new

    expose(:items) { Item.all }
  end
end

Rails will now search all the default paths and fall back on our new view resolver if nothing is found. This also means that should you want to modify how your items are rendered in the admin view all it takes is creating a new item partial in app/views/admin/items. Pretty slick if you ask me.

A Soap Box Moment

I must admit I had a bit of an ulterior motive for writing this post. Rails has been taking a lot of hits recently for a variety of reasons. I have my own thoughts about these debates but will need more time to distill my opinions and present them in a constructive fashion.

I did want to chime in on one topic however. There has been some discussion about the value of the Rails/Merb “merge” and whether or not it ended up being a net win. I feel like the problem Obie’s post addressed is a clear example of where Rails’ conventions can sometimes get in the way. I wrote this post as an attempt to illustrate how some of the philosophy taken from Merb can lead to better solutions.

Many of the comments I’ve read comparing Rails 3 to Rails 2 seem to be written through rose-colored glasses. It seems to me like people are not remembering what life was really like in Rails 2 land. There also seems to be a rather myopic view of the long term benefits the entire community reaps due to the architectural improvements in Rails 3.

The sort of monkey patching Obie proposed was rampant before Rails 3. And not just in application code, but in library code as well. This made every upgrade a minefield of potentially infuriating issues. By defining public interfaces to extend the framework everybody benefits from easier upgrades. This is a huge deal in my opinion.

That’s not to say things are perfect now. There is always room for improvement. I think we can do a better job highlighting new public extension API’s. Certain areas such as ActiveModel have gotten a lot more love than others. This post is my first attempt to shine some light on one of my favorite new API’s. Granted it’s a pretty trivial example. I plan to dig deeper in my next post.

Until then, I’d love for this post to start some discussion about what your favorite Rails 3 features are and why.