Thursday, November 5, 2009

Easy Partials in Rails

I created something at my job that has proven to be extremely useful that I think many people could benefit from. I like to call it Easy Partials, and the goal is to make using partials a bit easier (in case the name didn't make that glaringly obvious). The problem is that rendering a partial requires method calls when a little extra work will allow simpler and more readable partial invocation via convention.

You are probably lost, so let me give you some examples. As it stands today, to render a partial you would do:

<%= render :partial => "my_partial" %>


Which would take the partial "_my_partial.erb" from the same directory and render it.

It works, but what if you could take the whole "convention over configuration" idea and change it into:

<% _my_partial %>


A lot simpler, and pretty intuitive, right? Since partials by Rails conventions start with "_", it makes sense to just name a method as such to render the partial. Note that there is no "=" in the scriptlet, the _my_partial method will concat the partial, so you won't obtain a string to render.

We could create a helper method for every single partial we want to render like that, but that's rather cumbersome, isn't it? It's also not very DRY. You won't have exactly the same code, but you will find yourself with a lot of helpers that look rather similar. Instead, let's try overriding method_missing in our application_helper so we can avoid all those repeated helpers!

module ApplicationHelper
alias_method :method_missing_without_easy_partials, :method_missing

def method_missing_with_easy_partials(method_name, *args, &block)
method_str = method_name.to_s

if method_str =~ /^_.+$/
partial_name = method_str[/^_(.+)$/, 1]
concat_partial partial_name
else
method_missing_without_easy_partials method_name, *args, &block
end
end

alias_method :method_missing, :method_missing_with_easy_partials

# Concat the given partial.
def concat_partial(partial_name)
content = render :partial => partial_name
concat content
nil
end
end


What we've done here is check on method_missing to see if the method name starts with "_", and, if so, treat it as a partial and concat it. If the method doesn't start with "_", we fall back to the original method_missing implementation.

This works, but what if the partial needs some local variables? Before you would do:

<%= render :partial => "my_partial", :locals => { :var => "123" } %>


Instead, let's do:

<% _my_partial :var => "123" %>


Again, a lot simpler, and quite intuitive. To achieve this, our code will look like this:

module ApplicationHelper
alias_method :method_missing_without_easy_partials, :method_missing

def method_missing_with_easy_partials(method_name, *args, &block)
method_str = method_name.to_s

if method_str =~ /^_.+$/
partial_name = method_str[/^_(.+)$/, 1]
concat_partial partial_name, *args
else
method_missing_without_easy_partials method_name, *args, &block
end
end

alias_method :method_missing, :method_missing_with_easy_partials

# Concat the given partial.
def concat_partial(partial_name, options = {})
content = render :partial => partial_name, :locals => options
concat content
nil
end
end


Now we are passing the Hash passed in from the view on to concat_partial so we can specify the locals we want to render. We could check that there is no more than 1 argument passed into method_missing, but I prefer not to (feel free to use and improve anything you see here, in case that wasn't clear).

The next improvement we can make is to allow blocks to be passed in. There is no direct correlation for this next part, that I know of, except to build it yourself with helper methods. It was inspired by Ilya Grigorik.

Here is an example of what we will build:

<% _my_partial :var => "123" do %>
<p>
Some block content.
</p>
<% end %>


This will allow us to effectively pass in a block to the partial, so we can abstract some of the content in the partial so that the caller can define it. And now for the code:

module ApplicationHelper
alias_method :method_missing_without_easy_partials, :method_missing

def method_missing_with_easy_partials(method_name, *args, &block)
method_str = method_name.to_s

if method_str =~ /^_.+$/
partial_name = method_str[/^_(.+)$/, 1]
concat_partial partial_name, *args, &block
else
method_missing_without_easy_partials method_name, *args, &block
end
end

alias_method :method_missing, :method_missing_with_easy_partials

# Concat the given partial.
def concat_partial(partial_name, options = {}, &block)
unless block.nil?
options.merge! :body => capture(&block)
end

content = render :partial => partial_name, :locals => options
concat content
nil
end
end


Within your partial, you will use a "body" variable to output the contents of the block passed in. If you try to use a variable named "body" along with partial blocks, the block will override the body variable, so keep that in mind.

For the final improvement, consider a partial that belongs to more than one controller. What do we do then? Well, how about we maintain a shared directory that we pull from if the partial cannot be found within the local directory. Thus:

module ApplicationHelper
alias_method :method_missing_without_easy_partials, :method_missing

def method_missing_with_easy_partials(method_name, *args, &block)
method_str = method_name.to_s

if method_str =~ /^_.+$/
partial_name = method_str[/^_(.+)$/, 1]

begin
concat_partial partial_name, *args, &block
rescue ActionView::MissingTemplate
partial_name = "shared/#{partial_name}"
concat_partial partial_name, *args, &block
end
else
method_missing_without_easy_partials method_name, *args, &block
end
end

alias_method :method_missing, :method_missing_with_easy_partials

# Concat the given partial.
def concat_partial(partial_name, options = {}, &block)
unless block.nil?
options.merge! :body => capture(&block)
end

content = render :partial => partial_name, :locals => options
concat content
nil
end
end


So, if you use the _my_partial examples from above within the views for "person_controller", but there is no views/person/_my_partial.erb, it will fall back on views/shared/_my_partial.erb.

Using Easy Partials, you can avoid redundant helper methods, keep html in easy to access ERB templates, improve the readability of your views that use partials, and make your code more accessible for your non-programmer UI designers. Note that you can even invoke Easy Partials from within helper methods.

Special thanks to Ilya Grigorik's post on block helpers, which planted the seeds for Easy Partials.

3 comments:

Mike Stone said...

Here is the code on github:

http://github.com/mikestone/Easy-Partials

Anonymous said...

Do you need to alias method_missing? I don't think you do...

Mike Stone said...

Thanks for the comment Ed! The alias for method_missing isn't strictly necessary, but I did it to play well with others. For example, this will work with another plugin that defines method_missing, or if Rails itself is using method_missing.