Extending render with a collection
Partials offer a lot in the way of cutting out common code, especially when you couple them with a collection of objects. Take this code, for example:
While pretty nice, Rails has an easier way to do it, with the :collection option:
<!– items/list.rhtml –>
<!– items/_item.rhtml –>
Now that looks pretty sweet. Rails automatically takes the name of your partial and adds a local variable by the same name, setting its value to the current iteration through our collection. But… we've got a visual problem with our page. We have the margin-bottom CSS attribute on all div objects in the class item set to 1em. For the last item in the collection, we'd like to make the margin 0, to get rid of extra space. Rails gives you one local variable for free when you render a collection of partials, called {partial_name}_counter. In this case, the name of the counter would be item_counter and it would give you the index in the array for the current item being rendered. Using this variable, you could, in theory, do something like this in your partial:
<!– items/_item.rhtml –>
That's nice, but it's also pretty ugly. Also, what if you're rendering that partial in another view and you have not declared the @items variable? It's time to add our own auto-generated local variable to the render method. We're basically going to copy and paste Rails' default ActionView::Base.render method, but with one addition. You can put this code in your environment.rb file, if you'd like:
if options.is_a?(String)
render_file(options, true, old_local_assigns)
elsif options == :update
update_page(&block)
elsif options.is_a?(Hash)
options[:locals] ||= {}
options[:use_full_path] = options[:use_full_path].nil? ? true : options[:use_full_path]
if options[:file]
render_file(options[:file], options[:use_full_path], options[:locals])
elsif options[:partial] && options[:collection]
# This line is what we're concerned with. Note the addition of the 'last_item' local variable.
render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals].merge(:last_item => options[:collection].last))
elsif options[:partial]
render_partial(options[:partial], ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals])
elsif options[:inline]
render_template(options[:type] || :rhtml, options[:inline], nil, options[:locals] || {})
end
end
end
end
end
Now we get a local variable, for free, called last_item that we ca n use in our view to test for the last item in the collection:
<!– items/_item.rhtml –>
Obviously we can name the variable whatever we'd like, in case you happen to have a conflicting local variable called last_item. You could prepend the name of the partial to the variable name just to be safe, but I don't mind it, so I'll leave it as it is.