Mar 14, 2018

Wrapping rendered collections into a layout without a file

You’ve probably done this:

<%= render partial: 'product', collection: @products %>

Maybe you want to wrap that product partial in

<div class="col-sm-3">

on your home page, and

<div class="col-sm-2">

somewhere else. Solution? Pass a local or set some sort of instance variable. Ew, right?

There’s a little-known (at least, to me and the four people I spoke to) option to pass a layout for this partial.

<%= render partial: 'product', collection: @products, layout: 'home_layout' %>

would render products/_home_layout.html.erb:

<div class="col-sm-3"><%= yield %></div>

But I find that to be gross most of the time because so often these are just helper classes. So let’s break ActionView:

module ActionView
  class PartialRenderer < AbstractRenderer
    def find_template(path_or_source, locals)
      if path_or_source.index('{yield}')
        path_or_source.gsub!('{yield}', '<%=yield%>')
        return ActionView::Template.new(path_or_source, "fake_path", ActionView::Template::Handlers::ERB, @details)
      end
      prefixes = path_or_source.include?(?/) ? [] : @lookup_context.prefixes
      @lookup_context.find_template(path_or_source, prefixes, true, locals, @details)
    end
  end
end

And now, we can, perhaps naively,

<%= render partial: 'product', collection: @products, layout: '<div class="col-sm-3">{yield}</div>' %>

Note: Passing an ERB yield will remove where we want to actually yield.