« RSA (Retained Services Agreement) | Main | Can I Find an Agile, Design-Savvy iPhone Developer? »

January 30, 2012

Rendering Collections of Heterogeneous Objects in Rails 3.2

This commit by Grant Hutchins & Peter Jaros back in July 2011 gives ActiveModels the ability to define their own partial paths by implementing a to_partial_path method. (You can find a great explanation of this killer new feature on José Valim's blog post about it. Just scroll down to #4 Custom Partial Paths.)

I'm blogging about it here, because it impacts a technique that I described in May 2011 as Rendering Heterogeneous Collections of Objects in Rails. If you happen to be using that technique or anything like it involving the use of ActiveModel's partial_path method, you'll get deprecation warnings like this when upgrading to Rails 3.2:

DEPRECATION WARNING: partial_path is deprecated and will be removed from Rails 3.2 (ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead.). (called from partial_path at .../config/initializers/partial_path.rb:12)

In this blog post I describe how to update my technique and get rid of the deprecation warning.

The heterogeneous aspect of my technique had to do with rendering collections of STI objects that all inherited from a single Event base class. For organizational reasons, I wanted to put all my disparate event partials in a single app/view/events directory. Without the hack, they'd need to be in their own view/foo_events/_foo_event.html.erb partials. Ugh.

In Rails 3.2, instead of having to monkey patch the framework to fix that crap, I can just implement to_partial_path on the Event base class like this:

class Event

  def to_partial_path
    "events/#{self.class.name.underscore}"
  end 

end

Ahh, that's much cleaner. Unfortunately, there's still a catch, but only if you use controller namespaces.

Rails 3.2 keeps the annoying merge of the namespace prefix as part of the generated partial path even if you override it using to_partial_path. The good news is that it is easier to get rid of it in Rails 3.2 than before. The monkeypatch in my earlier blog post now becomes:

ActionView::PartialRenderer.class_eval do

  private

  def merge_prefix_into_object_path(pfx, path)
    path
  end 

end

What this little bit of duck punching does is to ensure that when you try to render a collection of objects, it will always return the same partial path regardless of whether you are inside a controller namespace or not. I feel that behavior should be default or at least optional (which is why I describe the feature as annoying.) Let me know if you think it's worth submitting a patch.

Incidentally, in his blog post mentioned above Jose mentions needing to add an as: parameter to your call to render, like this:

render :partial => @activities, :as => :activity

In my testing and examination of the underlying Rails code, it is clear that the :as option is not necessary. The PartialRenderer object always checks for the existence of a to_partial_path method on the object being rendered and calls it if present. 

One more thing. Currently the documentation states the following (italics mine):

Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work and pick the proper path by checking `to_proper_path` method. If the object passed to render is a collection, all objects must return the same path.

I'm not quite sure why it says that all objects must return the same path, or why that would even be considered desirable behavior (it's really not!) The good news is that I can verify for you that it does work to return different partial paths within the collection, in other words, you can still do the rendering heterogeneous objects trick. In fact, depending on your naming convention, you may even get correctly name local variables in your partial for free.

The following method determines what to call your partial's automatic local variable corresponding to the current item of the collection being rendered:

def retrieve_variable(path)
  variable =
    @options[:as].try(:to_sym) || path[%r'_?(\w+)(\.\w+)*$', 1].to_sym

  variable_counter = :"#{variable}_counter" if @collection
  [variable, variable_counter]
end

That regex is a little tricky, but whether it's the intentional behavior or not, it's going to grab whatever is between the last underscore of your partial name and the dot to use as the variable name. In the case of my own Event hierarchy, that means each partial will get its own event local variable, even though the partials are named _user_created_event, _nomination_event, etc.

Perfect. Now to hope someone doesn't fuck it up in a future release thinking it's a bug :)

Important Update: Check out my buddy Lar's take on this stuff in Rails 3 using view resolver classes so that you don't have to monkeypatch anything.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00e54fdca91188330168e661f9b8970c

Listed below are links to weblogs that reference Rendering Collections of Heterogeneous Objects in Rails 3.2:

Sponsored By

Flattr

or visit my my homepage

My Companies

My Latest Books

My Book Series